From 65c4ada11bbcffae3c352600c955289a2a1fa443 Mon Sep 17 00:00:00 2001 From: ImgBotApp Date: Sun, 20 Sep 2026 14:01:11 +0000 Subject: [PATCH 1/6] [ImgBot] Optimize images *Total -- 882.16kb -> 847.34kb (3.95%) /src/firefox/icons/providers/z_ai.svg -- 10.78kb -> 0.78kb (92.79%) /src/chrome/icons/providers/z_ai.svg -- 10.78kb -> 0.78kb (92.79%) /src/chrome/icons/flags/in.svg -- 1.06kb -> 0.91kb (14.59%) /web/assets/flags/in.svg -- 1.06kb -> 0.91kb (14.59%) /src/firefox/icons/flags/in.svg -- 1.06kb -> 0.91kb (14.59%) /src/chrome/icons/flags/kr.svg -- 1.04kb -> 0.91kb (12.25%) /src/firefox/icons/flags/kr.svg -- 1.04kb -> 0.91kb (12.25%) /web/assets/flags/kr.svg -- 1.04kb -> 0.91kb (12.25%) /src/firefox/icons/providers/stackit.svg -- 0.41kb -> 0.36kb (11.9%) /src/chrome/icons/providers/stackit.svg -- 0.41kb -> 0.36kb (11.9%) /src/chrome/icons/flags/ir.svg -- 15.04kb -> 13.67kb (9.09%) /web/assets/flags/ir.svg -- 15.04kb -> 13.67kb (9.09%) /src/firefox/icons/flags/ir.svg -- 15.04kb -> 13.67kb (9.09%) /src/chrome/icons/flags/jp.svg -- 0.46kb -> 0.42kb (8.72%) /src/firefox/icons/flags/jp.svg -- 0.46kb -> 0.42kb (8.72%) /web/assets/flags/jp.svg -- 0.46kb -> 0.42kb (8.72%) /src/chrome/icons/flags/my.svg -- 1.35kb -> 1.23kb (8.42%) /web/assets/flags/my.svg -- 1.35kb -> 1.23kb (8.42%) /src/firefox/icons/flags/my.svg -- 1.35kb -> 1.23kb (8.42%) /src/chrome/icons/flags/vn.svg -- 0.48kb -> 0.44kb (8.37%) /src/firefox/icons/flags/vn.svg -- 0.48kb -> 0.44kb (8.37%) /web/assets/flags/vn.svg -- 0.48kb -> 0.44kb (8.37%) /src/chrome/icons/flags/pl.svg -- 0.21kb -> 0.20kb (8.22%) /src/firefox/icons/flags/pl.svg -- 0.21kb -> 0.20kb (8.22%) /src/chrome/icons/providers/qihang-ai.svg -- 0.33kb -> 0.31kb (8.19%) /src/firefox/icons/providers/qihang-ai.svg -- 0.33kb -> 0.31kb (8.19%) /src/chrome/icons/flags/th.svg -- 0.28kb -> 0.25kb (8.1%) /web/assets/flags/th.svg -- 0.28kb -> 0.25kb (8.1%) /src/firefox/icons/flags/th.svg -- 0.28kb -> 0.25kb (8.1%) /src/chrome/icons/flags/ua.svg -- 0.23kb -> 0.21kb (7.76%) /web/assets/flags/ua.svg -- 0.23kb -> 0.21kb (7.76%) /src/firefox/icons/flags/ua.svg -- 0.23kb -> 0.21kb (7.76%) /src/chrome/icons/flags/il.svg -- 0.81kb -> 0.76kb (6.71%) /web/assets/flags/il.svg -- 0.81kb -> 0.76kb (6.71%) /src/firefox/icons/flags/il.svg -- 0.81kb -> 0.76kb (6.71%) /src/chrome/icons/providers/wandb.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/minimax-cn-coding-plan.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/google-vertex-anthropic.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/chutes.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/requesty.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/upstage.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/azure-cognitive-services.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/morph.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/clarifai.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/synthetic.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/azure-cognitive-services.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/minimax-coding-plan.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/requesty.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/cortecs.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/morph.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/upstage.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/stepfun.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/cortecs.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/wandb.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/clarifai.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/google-vertex-anthropic.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/providers/stepfun.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/minimax-cn-coding-plan.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/chutes.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/synthetic.svg -- 1.39kb -> 1.31kb (5.91%) /src/firefox/icons/providers/minimax-coding-plan.svg -- 1.39kb -> 1.31kb (5.91%) /src/chrome/icons/flags/tr.svg -- 0.54kb -> 0.51kb (5.1%) /web/assets/flags/tr.svg -- 0.54kb -> 0.51kb (5.1%) /src/firefox/icons/flags/tr.svg -- 0.54kb -> 0.51kb (5.1%) /web/assets/flags/de.svg -- 0.22kb -> 0.21kb (4.98%) /src/chrome/icons/flags/de.svg -- 0.22kb -> 0.21kb (4.98%) /src/firefox/icons/flags/de.svg -- 0.22kb -> 0.21kb (4.98%) /src/chrome/icons/flags/nl.svg -- 0.22kb -> 0.21kb (4.89%) /src/chrome/icons/flags/ru.svg -- 0.22kb -> 0.21kb (4.89%) /web/assets/flags/ru.svg -- 0.22kb -> 0.21kb (4.89%) /web/assets/flags/nl.svg -- 0.22kb -> 0.21kb (4.89%) /src/firefox/icons/flags/nl.svg -- 0.22kb -> 0.21kb (4.89%) /src/firefox/icons/flags/ru.svg -- 0.22kb -> 0.21kb (4.89%) /src/firefox/icons/providers/azure_openai.svg -- 1.58kb -> 1.50kb (4.83%) /src/chrome/icons/providers/azure_openai.svg -- 1.58kb -> 1.50kb (4.83%) /src/chrome/icons/flags/fr.svg -- 0.23kb -> 0.21kb (4.76%) /src/firefox/icons/flags/fr.svg -- 0.23kb -> 0.21kb (4.76%) /web/assets/flags/fr.svg -- 0.23kb -> 0.21kb (4.76%) /src/chrome/icons/providers/mistral.svg -- 0.64kb -> 0.61kb (4.58%) /web/assets/providers/mistral.svg -- 0.64kb -> 0.61kb (4.58%) /src/firefox/icons/providers/mistral.svg -- 0.64kb -> 0.61kb (4.58%) /src/chrome/icons/providers/local_openai_proxy.svg -- 0.43kb -> 0.41kb (4.5%) /src/firefox/icons/providers/local_openai_proxy.svg -- 0.43kb -> 0.41kb (4.5%) /src/chrome/icons/flags/id.svg -- 0.17kb -> 0.17kb (4.49%) /web/assets/flags/id.svg -- 0.17kb -> 0.17kb (4.49%) /src/firefox/icons/flags/id.svg -- 0.17kb -> 0.17kb (4.49%) /src/chrome/icons/flags/bd.svg -- 0.18kb -> 0.17kb (4.28%) /src/firefox/icons/flags/bd.svg -- 0.18kb -> 0.17kb (4.28%) /web/assets/flags/bd.svg -- 0.18kb -> 0.17kb (4.28%) /src/chrome/icons/providers/vllm.svg -- 0.28kb -> 0.27kb (4.2%) /src/firefox/icons/providers/vllm.svg -- 0.28kb -> 0.27kb (4.2%) /web/assets/providers/vllm.svg -- 0.28kb -> 0.27kb (4.2%) /src/chrome/icons/flags/us.svg -- 0.63kb -> 0.61kb (3.86%) /web/assets/flags/us.svg -- 0.63kb -> 0.61kb (3.86%) /src/firefox/icons/flags/us.svg -- 0.63kb -> 0.61kb (3.86%) /src/chrome/icons/flags/cn.svg -- 0.79kb -> 0.76kb (3.81%) /web/assets/flags/cn.svg -- 0.79kb -> 0.76kb (3.81%) /src/firefox/icons/flags/cn.svg -- 0.79kb -> 0.76kb (3.81%) /src/chrome/icons/flags/br.svg -- 6.97kb -> 6.73kb (3.43%) /web/assets/flags/br.svg -- 6.97kb -> 6.73kb (3.43%) /src/firefox/icons/flags/br.svg -- 6.97kb -> 6.73kb (3.43%) /src/firefox/icons/providers/qiniu-ai.svg -- 1.49kb -> 1.44kb (3.21%) /src/chrome/icons/providers/qiniu-ai.svg -- 1.49kb -> 1.44kb (3.21%) /src/chrome/icons/providers/novita-ai.svg -- 0.40kb -> 0.39kb (3.14%) /src/firefox/icons/providers/novita-ai.svg -- 0.40kb -> 0.39kb (3.14%) /src/chrome/icons/providers/together.svg -- 0.44kb -> 0.43kb (3.08%) /src/firefox/icons/providers/together.svg -- 0.44kb -> 0.43kb (3.08%) /src/chrome/icons/providers/gpt4all.svg -- 0.26kb -> 0.26kb (2.96%) /src/firefox/icons/providers/gpt4all.svg -- 0.26kb -> 0.26kb (2.96%) /src/chrome/icons/providers/kilo.svg -- 0.28kb -> 0.28kb (2.75%) /src/firefox/icons/providers/kilo.svg -- 0.28kb -> 0.28kb (2.75%) /src/chrome/icons/providers/webgpu.svg -- 0.52kb -> 0.50kb (2.65%) /src/chrome/icons/providers/digitalocean.svg -- 0.60kb -> 0.58kb (2.28%) /src/firefox/icons/providers/digitalocean.svg -- 0.60kb -> 0.58kb (2.28%) /src/firefox/icons/providers/gemini.svg -- 2.76kb -> 2.71kb (2.12%) /web/assets/providers/gemini.svg -- 2.76kb -> 2.71kb (2.12%) /src/chrome/icons/providers/gemini.svg -- 2.76kb -> 2.71kb (2.12%) /src/chrome/icons/flags/es.svg -- 79.06kb -> 77.47kb (2.01%) /src/firefox/icons/flags/es.svg -- 79.06kb -> 77.47kb (2.01%) /src/chrome/icons/providers/vercel.svg -- 0.16kb -> 0.16kb (1.81%) /src/firefox/icons/providers/vercel.svg -- 0.16kb -> 0.16kb (1.81%) /src/chrome/icons/providers/abacus.svg -- 2.81kb -> 2.76kb (1.74%) /src/firefox/icons/providers/abacus.svg -- 2.81kb -> 2.76kb (1.74%) /src/chrome/icons/providers/kimi.svg -- 0.69kb -> 0.68kb (1.69%) /src/firefox/icons/providers/kimi.svg -- 0.69kb -> 0.68kb (1.69%) /src/firefox/icons/providers/anthropic.svg -- 0.36kb -> 0.35kb (1.63%) /web/assets/providers/anthropic.svg -- 0.36kb -> 0.35kb (1.63%) /src/chrome/icons/providers/anthropic.svg -- 0.36kb -> 0.35kb (1.63%) /src/chrome/icons/providers/xai.svg -- 0.36kb -> 0.36kb (1.61%) /web/assets/providers/xai.svg -- 0.36kb -> 0.36kb (1.61%) /src/firefox/icons/providers/xai.svg -- 0.36kb -> 0.36kb (1.61%) /src/chrome/icons/providers/cloudferro-sherlock.svg -- 0.40kb -> 0.39kb (1.47%) /src/firefox/icons/providers/cloudferro-sherlock.svg -- 0.40kb -> 0.39kb (1.47%) /src/chrome/icons/providers/jiekou.svg -- 0.28kb -> 0.27kb (1.4%) /src/firefox/icons/providers/jiekou.svg -- 0.28kb -> 0.27kb (1.4%) /src/chrome/icons/providers/opencode-go.svg -- 0.22kb -> 0.22kb (1.32%) /src/firefox/icons/providers/opencode-go.svg -- 0.22kb -> 0.22kb (1.32%) /src/chrome/icons/providers/inception.svg -- 0.32kb -> 0.31kb (1.23%) /src/firefox/icons/providers/inception.svg -- 0.32kb -> 0.31kb (1.23%) /src/chrome/icons/providers/drun.svg -- 1.59kb -> 1.57kb (1.23%) /src/firefox/icons/providers/drun.svg -- 1.59kb -> 1.57kb (1.23%) /src/chrome/icons/providers/minimax.svg -- 1.53kb -> 1.51kb (1.15%) /src/firefox/icons/providers/minimax.svg -- 1.53kb -> 1.51kb (1.15%) /src/chrome/icons/providers/cloudflare.svg -- 1.05kb -> 1.04kb (1.12%) /src/firefox/icons/providers/cloudflare.svg -- 1.05kb -> 1.04kb (1.12%) /src/chrome/icons/flags/sa.svg -- 9.73kb -> 9.63kb (1.11%) /web/assets/flags/sa.svg -- 9.73kb -> 9.63kb (1.11%) /src/firefox/icons/flags/sa.svg -- 9.73kb -> 9.63kb (1.11%) /src/chrome/icons/providers/opencode.svg -- 0.27kb -> 0.27kb (1.09%) /src/firefox/icons/providers/opencode.svg -- 0.27kb -> 0.27kb (1.09%) /src/chrome/icons/providers/zhipuai-coding-plan.svg -- 0.27kb -> 0.27kb (1.08%) /src/chrome/icons/providers/kuae-cloud-coding-plan.svg -- 0.27kb -> 0.27kb (1.08%) /src/firefox/icons/providers/zhipuai-coding-plan.svg -- 0.27kb -> 0.27kb (1.08%) /src/firefox/icons/providers/kuae-cloud-coding-plan.svg -- 0.27kb -> 0.27kb (1.08%) /src/firefox/icons/providers/aws_bedrock.svg -- 2.21kb -> 2.19kb (1.06%) /src/chrome/icons/providers/aws_bedrock.svg -- 2.21kb -> 2.19kb (1.06%) /src/firefox/icons/providers/groq.svg -- 0.55kb -> 0.55kb (1.06%) /src/chrome/icons/providers/groq.svg -- 0.55kb -> 0.55kb (1.06%) /src/firefox/icons/providers/302ai.svg -- 4.92kb -> 4.87kb (1.05%) /src/chrome/icons/providers/302ai.svg -- 4.92kb -> 4.87kb (1.05%) /src/chrome/icons/providers/zhipuai.svg -- 0.29kb -> 0.29kb (1.02%) /src/chrome/icons/providers/zai-coding-plan.svg -- 0.29kb -> 0.29kb (1.02%) /src/firefox/icons/providers/zai-coding-plan.svg -- 0.29kb -> 0.29kb (1.02%) /src/firefox/icons/providers/zhipuai.svg -- 0.29kb -> 0.29kb (1.02%) /src/chrome/icons/providers/fireworks.svg -- 0.58kb -> 0.57kb (1.01%) /src/firefox/icons/providers/fireworks.svg -- 0.58kb -> 0.57kb (1.01%) /src/chrome/icons/flags/ph.svg -- 1.35kb -> 1.34kb (1.01%) /web/assets/flags/ph.svg -- 1.35kb -> 1.34kb (1.01%) /src/firefox/icons/flags/ph.svg -- 1.35kb -> 1.34kb (1.01%) /src/chrome/icons/providers/lmstudio.svg -- 1.22kb -> 1.21kb (0.96%) /src/firefox/icons/providers/lmstudio.svg -- 1.22kb -> 1.21kb (0.96%) /web/assets/providers/lmstudio.svg -- 1.22kb -> 1.21kb (0.96%) /src/chrome/icons/providers/inference.svg -- 0.31kb -> 0.30kb (0.96%) /src/chrome/icons/providers/huggingface.svg -- 3.68kb -> 3.64kb (0.96%) /src/firefox/icons/providers/inference.svg -- 0.31kb -> 0.30kb (0.96%) /src/firefox/icons/providers/huggingface.svg -- 3.68kb -> 3.64kb (0.96%) /src/chrome/icons/providers/zenmux.svg -- 2.47kb -> 2.45kb (0.95%) /src/firefox/icons/providers/zenmux.svg -- 2.47kb -> 2.45kb (0.95%) /src/chrome/icons/providers/helicone.svg -- 1.24kb -> 1.23kb (0.95%) /src/firefox/icons/providers/helicone.svg -- 1.24kb -> 1.23kb (0.95%) /src/firefox/icons/providers/alibaba.svg -- 1.99kb -> 1.98kb (0.88%) /src/chrome/icons/providers/alibaba.svg -- 1.99kb -> 1.98kb (0.88%) /web/assets/marktechpost.svg -- 0.59kb -> 0.58kb (0.83%) /src/chrome/icons/providers/io-net.svg -- 0.63kb -> 0.63kb (0.78%) /src/firefox/icons/providers/io-net.svg -- 0.63kb -> 0.63kb (0.78%) /web/assets/browser-chrome.svg -- 1.40kb -> 1.39kb (0.77%) /src/chrome/icons/providers/orcarouter.svg -- 1.52kb -> 1.51kb (0.71%) /src/firefox/icons/providers/orcarouter.svg -- 1.52kb -> 1.51kb (0.71%) /src/chrome/icons/providers/scaleway.svg -- 1.52kb -> 1.51kb (0.7%) /src/firefox/icons/providers/scaleway.svg -- 1.52kb -> 1.51kb (0.7%) /src/chrome/icons/providers/ovhcloud.svg -- 0.42kb -> 0.42kb (0.7%) /src/firefox/icons/providers/ovhcloud.svg -- 0.42kb -> 0.42kb (0.7%) /src/chrome/icons/providers/berget.svg -- 0.28kb -> 0.28kb (0.7%) /src/firefox/icons/providers/berget.svg -- 0.28kb -> 0.28kb (0.7%) /src/chrome/icons/providers/modelscope.svg -- 0.71kb -> 0.70kb (0.69%) /src/firefox/icons/providers/modelscope.svg -- 0.71kb -> 0.70kb (0.69%) /src/chrome/icons/providers/nano-gpt.svg -- 0.58kb -> 0.58kb (0.67%) /src/firefox/icons/providers/nano-gpt.svg -- 0.58kb -> 0.58kb (0.67%) /src/chrome/icons/providers/perplexity-agent.svg -- 0.59kb -> 0.59kb (0.66%) /src/chrome/icons/providers/openrouter.svg -- 0.88kb -> 0.88kb (0.66%) /src/chrome/icons/providers/perplexity.svg -- 0.59kb -> 0.59kb (0.66%) /src/firefox/icons/providers/perplexity.svg -- 0.59kb -> 0.59kb (0.66%) /src/firefox/icons/providers/perplexity-agent.svg -- 0.59kb -> 0.59kb (0.66%) /src/firefox/icons/providers/openrouter.svg -- 0.88kb -> 0.88kb (0.66%) /web/assets/providers/openrouter.svg -- 0.88kb -> 0.88kb (0.66%) /src/chrome/icons/providers/fastrouter.svg -- 0.48kb -> 0.47kb (0.61%) /src/firefox/icons/providers/fastrouter.svg -- 0.48kb -> 0.47kb (0.61%) /src/chrome/icons/providers/nebius.svg -- 0.64kb -> 0.64kb (0.61%) /src/firefox/icons/providers/nebius.svg -- 0.64kb -> 0.64kb (0.61%) /src/chrome/icons/providers/submodel.svg -- 0.82kb -> 0.81kb (0.6%) /src/firefox/icons/providers/submodel.svg -- 0.82kb -> 0.81kb (0.6%) /src/chrome/icons/providers/vivgrid.svg -- 1.33kb -> 1.32kb (0.59%) /src/firefox/icons/providers/vivgrid.svg -- 1.33kb -> 1.32kb (0.59%) /src/firefox/icons/providers/nvidia.svg -- 1.00kb -> 1.00kb (0.59%) /src/chrome/icons/providers/nvidia.svg -- 1.00kb -> 1.00kb (0.59%) /src/chrome/icons/providers/cohere.svg -- 0.89kb -> 0.89kb (0.55%) /src/firefox/icons/providers/cohere.svg -- 0.89kb -> 0.89kb (0.55%) /src/chrome/icons/providers/tencent-coding-plan.svg -- 2.02kb -> 2.01kb (0.53%) /src/firefox/icons/providers/tencent-coding-plan.svg -- 2.02kb -> 2.01kb (0.53%) /src/chrome/icons/providers/llamacpp.svg -- 1.29kb -> 1.28kb (0.53%) /web/assets/providers/llamacpp.svg -- 1.29kb -> 1.28kb (0.53%) /src/firefox/icons/providers/llamacpp.svg -- 1.29kb -> 1.28kb (0.53%) /src/firefox/icons/providers/nearai.svg -- 0.19kb -> 0.19kb (0.52%) /src/chrome/icons/providers/nearai.svg -- 0.19kb -> 0.19kb (0.52%) /src/firefox/icons/providers/privatemode-ai.svg -- 0.81kb -> 0.80kb (0.48%) /src/chrome/icons/providers/privatemode-ai.svg -- 0.81kb -> 0.80kb (0.48%) /src/chrome/icons/providers/dinference.svg -- 0.22kb -> 0.21kb (0.45%) /src/firefox/icons/providers/dinference.svg -- 0.22kb -> 0.21kb (0.45%) /src/chrome/icons/providers/evroc.svg -- 2.51kb -> 2.50kb (0.43%) /src/firefox/icons/providers/evroc.svg -- 2.51kb -> 2.50kb (0.43%) /src/chrome/icons/providers/siliconflow-cn.svg -- 0.76kb -> 0.76kb (0.38%) /src/firefox/icons/providers/siliconflow.svg -- 0.76kb -> 0.76kb (0.38%) /src/firefox/icons/providers/siliconflow-cn.svg -- 0.76kb -> 0.76kb (0.38%) /src/chrome/icons/providers/siliconflow.svg -- 0.76kb -> 0.76kb (0.38%) /src/chrome/icons/providers/v0.svg -- 0.79kb -> 0.79kb (0.37%) /src/firefox/icons/providers/v0.svg -- 0.79kb -> 0.79kb (0.37%) /src/chrome/icons/providers/openai.svg -- 1.67kb -> 1.66kb (0.35%) /web/assets/providers/openai.svg -- 1.67kb -> 1.66kb (0.35%) /src/firefox/icons/providers/openai.svg -- 1.67kb -> 1.66kb (0.35%) /src/chrome/icons/providers/nova.svg -- 1.58kb -> 1.57kb (0.31%) /src/firefox/icons/providers/nova.svg -- 1.58kb -> 1.57kb (0.31%) /src/chrome/icons/providers/deepseek.svg -- 2.11kb -> 2.11kb (0.28%) /web/assets/providers/deepseek.svg -- 2.11kb -> 2.11kb (0.28%) /src/firefox/icons/providers/deepseek.svg -- 2.11kb -> 2.11kb (0.28%) /src/chrome/icons/providers/vultr.svg -- 2.17kb -> 2.16kb (0.27%) /src/firefox/icons/providers/vultr.svg -- 2.17kb -> 2.16kb (0.27%) /src/chrome/icons/providers/llama.svg -- 1.13kb -> 1.12kb (0.26%) /src/firefox/icons/providers/llama.svg -- 1.13kb -> 1.12kb (0.26%) /src/chrome/icons/providers/poe.svg -- 3.61kb -> 3.60kb (0.24%) /src/firefox/icons/providers/poe.svg -- 3.61kb -> 3.60kb (0.24%) /src/chrome/icons/providers/friendli.svg -- 2.14kb -> 2.14kb (0.23%) /src/firefox/icons/providers/friendli.svg -- 2.14kb -> 2.14kb (0.23%) /src/chrome/icons/providers/unsloth.svg -- 16.12kb -> 16.09kb (0.22%) /src/firefox/icons/providers/unsloth.svg -- 16.12kb -> 16.09kb (0.22%) /src/chrome/icons/providers/bailing.svg -- 1.59kb -> 1.59kb (0.18%) /src/firefox/icons/providers/bailing.svg -- 1.59kb -> 1.59kb (0.18%) /src/chrome/icons/providers/baseten.svg -- 1.59kb -> 1.59kb (0.18%) /src/firefox/icons/providers/baseten.svg -- 1.59kb -> 1.59kb (0.18%) /src/chrome/icons/providers/ollama.svg -- 3.21kb -> 3.21kb (0.18%) /src/firefox/icons/providers/ollama.svg -- 3.21kb -> 3.21kb (0.18%) /web/assets/providers/ollama.svg -- 3.21kb -> 3.21kb (0.18%) /src/chrome/icons/providers/xiaomi.svg -- 1.69kb -> 1.69kb (0.17%) /src/firefox/icons/providers/xiaomi.svg -- 1.69kb -> 1.69kb (0.17%) /src/chrome/icons/providers/alibaba-coding-plan-cn.svg -- 2.01kb -> 2.01kb (0.15%) /src/chrome/icons/providers/alibaba-coding-plan.svg -- 2.01kb -> 2.01kb (0.15%) /src/firefox/icons/providers/alibaba-coding-plan-cn.svg -- 2.01kb -> 2.01kb (0.15%) /src/firefox/icons/providers/alibaba-coding-plan.svg -- 2.01kb -> 2.01kb (0.15%) /src/chrome/icons/providers/aihubmix.svg -- 2.19kb -> 2.19kb (0.13%) /src/firefox/icons/providers/aihubmix.svg -- 2.19kb -> 2.19kb (0.13%) /src/chrome/icons/providers/moark.svg -- 2.25kb -> 2.25kb (0.13%) /src/firefox/icons/providers/moark.svg -- 2.25kb -> 2.25kb (0.13%) /src/chrome/icons/providers/ollama-cloud.svg -- 5.54kb -> 5.54kb (0.12%) /src/firefox/icons/providers/ollama-cloud.svg -- 5.54kb -> 5.54kb (0.12%) /src/chrome/icons/providers/cerebras.svg -- 3.28kb -> 3.27kb (0.12%) /src/firefox/icons/providers/cerebras.svg -- 3.28kb -> 3.27kb (0.12%) /src/chrome/icons/providers/iflowcn.svg -- 2.61kb -> 2.60kb (0.11%) /src/firefox/icons/providers/iflowcn.svg -- 2.61kb -> 2.60kb (0.11%) /src/chrome/icons/providers/venice.svg -- 4.24kb -> 4.23kb (0.09%) /src/firefox/icons/providers/venice.svg -- 4.24kb -> 4.23kb (0.09%) /src/chrome/icons/providers/google-vertex.svg -- 10.98kb -> 10.97kb (0.09%) /src/firefox/icons/providers/google-vertex.svg -- 10.98kb -> 10.97kb (0.09%) /src/chrome/icons/providers/kimi-for-coding.svg -- 6.65kb -> 6.65kb (0.04%) /src/firefox/icons/providers/kimi-for-coding.svg -- 6.65kb -> 6.65kb (0.04%) /src/chrome/icons/providers/pollinations.svg -- 7.27kb -> 7.26kb (0.03%) /src/firefox/icons/providers/pollinations.svg -- 7.27kb -> 7.26kb (0.03%) /src/chrome/icons/providers/deepinfra.svg -- 14.55kb -> 14.54kb (0.03%) /src/firefox/icons/providers/deepinfra.svg -- 14.55kb -> 14.54kb (0.03%) /web/assets/browser-edge.svg -- 3.72kb -> 3.72kb (0.03%) /src/chrome/icons/providers/meganova.svg -- 11.54kb -> 11.54kb (0.03%) /src/firefox/icons/providers/meganova.svg -- 11.54kb -> 11.54kb (0.03%) /web/assets/browser-firefox.svg -- 10.23kb -> 10.23kb (0.01%) /src/chrome/icons/providers/lucidquery.svg -- 78.13kb -> 78.13kb (0%) /src/firefox/icons/providers/lucidquery.svg -- 78.13kb -> 78.13kb (0%) Signed-off-by: ImgBotApp --- src/chrome/icons/flags/bd.svg | 5 +- src/chrome/icons/flags/br.svg | 46 +- src/chrome/icons/flags/cn.svg | 12 +- src/chrome/icons/flags/de.svg | 6 +- src/chrome/icons/flags/es.svg | 545 +----------------- src/chrome/icons/flags/fr.svg | 6 +- src/chrome/icons/flags/id.svg | 5 +- src/chrome/icons/flags/il.svg | 15 +- src/chrome/icons/flags/in.svg | 26 +- src/chrome/icons/flags/ir.svg | 220 +------ src/chrome/icons/flags/jp.svg | 12 +- src/chrome/icons/flags/kr.svg | 25 +- src/chrome/icons/flags/my.svg | 27 +- src/chrome/icons/flags/nl.svg | 6 +- src/chrome/icons/flags/ph.svg | 7 +- src/chrome/icons/flags/pl.svg | 7 +- src/chrome/icons/flags/ru.svg | 6 +- src/chrome/icons/flags/sa.svg | 26 +- src/chrome/icons/flags/th.svg | 8 +- src/chrome/icons/flags/tr.svg | 9 +- src/chrome/icons/flags/ua.svg | 7 +- src/chrome/icons/flags/us.svg | 10 +- src/chrome/icons/flags/vn.svg | 12 +- src/chrome/icons/providers/302ai.svg | 8 +- src/chrome/icons/providers/abacus.svg | 15 +- src/chrome/icons/providers/aihubmix.svg | 4 +- .../providers/alibaba-coding-plan-cn.svg | 4 +- .../icons/providers/alibaba-coding-plan.svg | 4 +- src/chrome/icons/providers/alibaba.svg | 2 +- src/chrome/icons/providers/anthropic.svg | 2 +- src/chrome/icons/providers/aws_bedrock.svg | 2 +- .../providers/azure-cognitive-services.svg | 25 +- src/chrome/icons/providers/azure_openai.svg | 2 +- src/chrome/icons/providers/bailing.svg | 4 +- src/chrome/icons/providers/baseten.svg | 4 +- src/chrome/icons/providers/berget.svg | 4 +- src/chrome/icons/providers/cerebras.svg | 5 +- src/chrome/icons/providers/chutes.svg | 25 +- src/chrome/icons/providers/clarifai.svg | 25 +- .../icons/providers/cloudferro-sherlock.svg | 6 +- src/chrome/icons/providers/cloudflare.svg | 2 +- src/chrome/icons/providers/cohere.svg | 6 +- src/chrome/icons/providers/cortecs.svg | 25 +- src/chrome/icons/providers/deepinfra.svg | 5 +- src/chrome/icons/providers/deepseek.svg | 2 +- src/chrome/icons/providers/digitalocean.svg | 7 +- src/chrome/icons/providers/dinference.svg | 2 +- src/chrome/icons/providers/drun.svg | 9 +- src/chrome/icons/providers/evroc.svg | 4 +- src/chrome/icons/providers/fastrouter.svg | 4 +- src/chrome/icons/providers/fireworks.svg | 2 +- src/chrome/icons/providers/friendli.svg | 4 +- src/chrome/icons/providers/gemini.svg | 2 +- .../providers/google-vertex-anthropic.svg | 25 +- src/chrome/icons/providers/google-vertex.svg | 11 +- src/chrome/icons/providers/gpt4all.svg | 5 +- src/chrome/icons/providers/groq.svg | 2 +- src/chrome/icons/providers/helicone.svg | 7 +- src/chrome/icons/providers/huggingface.svg | 2 +- src/chrome/icons/providers/iflowcn.svg | 4 +- src/chrome/icons/providers/inception.svg | 5 +- src/chrome/icons/providers/inference.svg | 4 +- src/chrome/icons/providers/io-net.svg | 6 +- src/chrome/icons/providers/jiekou.svg | 5 +- src/chrome/icons/providers/kilo.svg | 5 +- .../icons/providers/kimi-for-coding.svg | 4 +- src/chrome/icons/providers/kimi.svg | 2 +- .../providers/kuae-cloud-coding-plan.svg | 4 +- src/chrome/icons/providers/llama.svg | 4 +- src/chrome/icons/providers/llamacpp.svg | 8 +- src/chrome/icons/providers/lmstudio.svg | 2 +- .../icons/providers/local_openai_proxy.svg | 9 +- src/chrome/icons/providers/lucidquery.svg | 4 +- src/chrome/icons/providers/meganova.svg | 4 +- .../providers/minimax-cn-coding-plan.svg | 25 +- .../icons/providers/minimax-coding-plan.svg | 25 +- src/chrome/icons/providers/minimax.svg | 2 +- src/chrome/icons/providers/mistral.svg | 2 +- src/chrome/icons/providers/moark.svg | 4 +- src/chrome/icons/providers/modelscope.svg | 6 +- src/chrome/icons/providers/morph.svg | 25 +- src/chrome/icons/providers/nano-gpt.svg | 4 +- src/chrome/icons/providers/nearai.svg | 2 +- src/chrome/icons/providers/nebius.svg | 5 +- src/chrome/icons/providers/nova.svg | 4 +- src/chrome/icons/providers/novita-ai.svg | 11 +- src/chrome/icons/providers/nvidia.svg | 2 +- src/chrome/icons/providers/ollama-cloud.svg | 8 +- src/chrome/icons/providers/ollama.svg | 2 +- src/chrome/icons/providers/openai.svg | 2 +- src/chrome/icons/providers/opencode-go.svg | 4 +- src/chrome/icons/providers/opencode.svg | 4 +- src/chrome/icons/providers/openrouter.svg | 2 +- src/chrome/icons/providers/orcarouter.svg | 6 +- src/chrome/icons/providers/ovhcloud.svg | 4 +- .../icons/providers/perplexity-agent.svg | 4 +- src/chrome/icons/providers/perplexity.svg | 4 +- src/chrome/icons/providers/poe.svg | 8 +- src/chrome/icons/providers/pollinations.svg | 2 +- src/chrome/icons/providers/privatemode-ai.svg | 6 +- src/chrome/icons/providers/qihang-ai.svg | 10 +- src/chrome/icons/providers/qiniu-ai.svg | 8 +- src/chrome/icons/providers/requesty.svg | 25 +- src/chrome/icons/providers/scaleway.svg | 6 +- src/chrome/icons/providers/siliconflow-cn.svg | 4 +- src/chrome/icons/providers/siliconflow.svg | 4 +- src/chrome/icons/providers/stackit.svg | 5 +- src/chrome/icons/providers/stepfun.svg | 25 +- src/chrome/icons/providers/submodel.svg | 4 +- src/chrome/icons/providers/synthetic.svg | 25 +- .../icons/providers/tencent-coding-plan.svg | 6 +- src/chrome/icons/providers/together.svg | 2 +- src/chrome/icons/providers/unsloth.svg | 17 +- src/chrome/icons/providers/upstage.svg | 25 +- src/chrome/icons/providers/v0.svg | 4 +- src/chrome/icons/providers/venice.svg | 5 +- src/chrome/icons/providers/vercel.svg | 4 +- src/chrome/icons/providers/vivgrid.svg | 5 +- src/chrome/icons/providers/vllm.svg | 2 +- src/chrome/icons/providers/vultr.svg | 7 +- src/chrome/icons/providers/wandb.svg | 25 +- src/chrome/icons/providers/webgpu.svg | 7 +- src/chrome/icons/providers/xai.svg | 2 +- src/chrome/icons/providers/xiaomi.svg | 4 +- src/chrome/icons/providers/z_ai.svg | 220 +------ .../icons/providers/zai-coding-plan.svg | 4 +- src/chrome/icons/providers/zenmux.svg | 5 +- .../icons/providers/zhipuai-coding-plan.svg | 4 +- src/chrome/icons/providers/zhipuai.svg | 4 +- src/firefox/icons/flags/bd.svg | 5 +- src/firefox/icons/flags/br.svg | 46 +- src/firefox/icons/flags/cn.svg | 12 +- src/firefox/icons/flags/de.svg | 6 +- src/firefox/icons/flags/es.svg | 545 +----------------- src/firefox/icons/flags/fr.svg | 6 +- src/firefox/icons/flags/id.svg | 5 +- src/firefox/icons/flags/il.svg | 15 +- src/firefox/icons/flags/in.svg | 26 +- src/firefox/icons/flags/ir.svg | 220 +------ src/firefox/icons/flags/jp.svg | 12 +- src/firefox/icons/flags/kr.svg | 25 +- src/firefox/icons/flags/my.svg | 27 +- src/firefox/icons/flags/nl.svg | 6 +- src/firefox/icons/flags/ph.svg | 7 +- src/firefox/icons/flags/pl.svg | 7 +- src/firefox/icons/flags/ru.svg | 6 +- src/firefox/icons/flags/sa.svg | 26 +- src/firefox/icons/flags/th.svg | 8 +- src/firefox/icons/flags/tr.svg | 9 +- src/firefox/icons/flags/ua.svg | 7 +- src/firefox/icons/flags/us.svg | 10 +- src/firefox/icons/flags/vn.svg | 12 +- src/firefox/icons/providers/302ai.svg | 8 +- src/firefox/icons/providers/abacus.svg | 15 +- src/firefox/icons/providers/aihubmix.svg | 4 +- .../providers/alibaba-coding-plan-cn.svg | 4 +- .../icons/providers/alibaba-coding-plan.svg | 4 +- src/firefox/icons/providers/alibaba.svg | 2 +- src/firefox/icons/providers/anthropic.svg | 2 +- src/firefox/icons/providers/aws_bedrock.svg | 2 +- .../providers/azure-cognitive-services.svg | 25 +- src/firefox/icons/providers/azure_openai.svg | 2 +- src/firefox/icons/providers/bailing.svg | 4 +- src/firefox/icons/providers/baseten.svg | 4 +- src/firefox/icons/providers/berget.svg | 4 +- src/firefox/icons/providers/cerebras.svg | 5 +- src/firefox/icons/providers/chutes.svg | 25 +- src/firefox/icons/providers/clarifai.svg | 25 +- .../icons/providers/cloudferro-sherlock.svg | 6 +- src/firefox/icons/providers/cloudflare.svg | 2 +- src/firefox/icons/providers/cohere.svg | 6 +- src/firefox/icons/providers/cortecs.svg | 25 +- src/firefox/icons/providers/deepinfra.svg | 5 +- src/firefox/icons/providers/deepseek.svg | 2 +- src/firefox/icons/providers/digitalocean.svg | 7 +- src/firefox/icons/providers/dinference.svg | 2 +- src/firefox/icons/providers/drun.svg | 9 +- src/firefox/icons/providers/evroc.svg | 4 +- src/firefox/icons/providers/fastrouter.svg | 4 +- src/firefox/icons/providers/fireworks.svg | 2 +- src/firefox/icons/providers/friendli.svg | 4 +- src/firefox/icons/providers/gemini.svg | 2 +- .../providers/google-vertex-anthropic.svg | 25 +- src/firefox/icons/providers/google-vertex.svg | 11 +- src/firefox/icons/providers/gpt4all.svg | 5 +- src/firefox/icons/providers/groq.svg | 2 +- src/firefox/icons/providers/helicone.svg | 7 +- src/firefox/icons/providers/huggingface.svg | 2 +- src/firefox/icons/providers/iflowcn.svg | 4 +- src/firefox/icons/providers/inception.svg | 5 +- src/firefox/icons/providers/inference.svg | 4 +- src/firefox/icons/providers/io-net.svg | 6 +- src/firefox/icons/providers/jiekou.svg | 5 +- src/firefox/icons/providers/kilo.svg | 5 +- .../icons/providers/kimi-for-coding.svg | 4 +- src/firefox/icons/providers/kimi.svg | 2 +- .../providers/kuae-cloud-coding-plan.svg | 4 +- src/firefox/icons/providers/llama.svg | 4 +- src/firefox/icons/providers/llamacpp.svg | 8 +- src/firefox/icons/providers/lmstudio.svg | 2 +- .../icons/providers/local_openai_proxy.svg | 9 +- src/firefox/icons/providers/lucidquery.svg | 4 +- src/firefox/icons/providers/meganova.svg | 4 +- .../providers/minimax-cn-coding-plan.svg | 25 +- .../icons/providers/minimax-coding-plan.svg | 25 +- src/firefox/icons/providers/minimax.svg | 2 +- src/firefox/icons/providers/mistral.svg | 2 +- src/firefox/icons/providers/moark.svg | 4 +- src/firefox/icons/providers/modelscope.svg | 6 +- src/firefox/icons/providers/morph.svg | 25 +- src/firefox/icons/providers/nano-gpt.svg | 4 +- src/firefox/icons/providers/nearai.svg | 2 +- src/firefox/icons/providers/nebius.svg | 5 +- src/firefox/icons/providers/nova.svg | 4 +- src/firefox/icons/providers/novita-ai.svg | 11 +- src/firefox/icons/providers/nvidia.svg | 2 +- src/firefox/icons/providers/ollama-cloud.svg | 8 +- src/firefox/icons/providers/ollama.svg | 2 +- src/firefox/icons/providers/openai.svg | 2 +- src/firefox/icons/providers/opencode-go.svg | 4 +- src/firefox/icons/providers/opencode.svg | 4 +- src/firefox/icons/providers/openrouter.svg | 2 +- src/firefox/icons/providers/orcarouter.svg | 6 +- src/firefox/icons/providers/ovhcloud.svg | 4 +- .../icons/providers/perplexity-agent.svg | 4 +- src/firefox/icons/providers/perplexity.svg | 4 +- src/firefox/icons/providers/poe.svg | 8 +- src/firefox/icons/providers/pollinations.svg | 2 +- .../icons/providers/privatemode-ai.svg | 6 +- src/firefox/icons/providers/qihang-ai.svg | 10 +- src/firefox/icons/providers/qiniu-ai.svg | 8 +- src/firefox/icons/providers/requesty.svg | 25 +- src/firefox/icons/providers/scaleway.svg | 6 +- .../icons/providers/siliconflow-cn.svg | 4 +- src/firefox/icons/providers/siliconflow.svg | 4 +- src/firefox/icons/providers/stackit.svg | 5 +- src/firefox/icons/providers/stepfun.svg | 25 +- src/firefox/icons/providers/submodel.svg | 4 +- src/firefox/icons/providers/synthetic.svg | 25 +- .../icons/providers/tencent-coding-plan.svg | 6 +- src/firefox/icons/providers/together.svg | 2 +- src/firefox/icons/providers/unsloth.svg | 17 +- src/firefox/icons/providers/upstage.svg | 25 +- src/firefox/icons/providers/v0.svg | 4 +- src/firefox/icons/providers/venice.svg | 5 +- src/firefox/icons/providers/vercel.svg | 4 +- src/firefox/icons/providers/vivgrid.svg | 5 +- src/firefox/icons/providers/vllm.svg | 2 +- src/firefox/icons/providers/vultr.svg | 7 +- src/firefox/icons/providers/wandb.svg | 25 +- src/firefox/icons/providers/xai.svg | 2 +- src/firefox/icons/providers/xiaomi.svg | 4 +- src/firefox/icons/providers/z_ai.svg | 220 +------ .../icons/providers/zai-coding-plan.svg | 4 +- src/firefox/icons/providers/zenmux.svg | 5 +- .../icons/providers/zhipuai-coding-plan.svg | 4 +- src/firefox/icons/providers/zhipuai.svg | 4 +- web/assets/browser-chrome.svg | 2 +- web/assets/browser-edge.svg | 2 +- web/assets/browser-firefox.svg | 2 +- web/assets/flags/bd.svg | 5 +- web/assets/flags/br.svg | 46 +- web/assets/flags/cn.svg | 12 +- web/assets/flags/de.svg | 6 +- web/assets/flags/fr.svg | 6 +- web/assets/flags/id.svg | 5 +- web/assets/flags/il.svg | 15 +- web/assets/flags/in.svg | 26 +- web/assets/flags/ir.svg | 220 +------ web/assets/flags/jp.svg | 12 +- web/assets/flags/kr.svg | 25 +- web/assets/flags/my.svg | 27 +- web/assets/flags/nl.svg | 6 +- web/assets/flags/ph.svg | 7 +- web/assets/flags/ru.svg | 6 +- web/assets/flags/sa.svg | 26 +- web/assets/flags/th.svg | 8 +- web/assets/flags/tr.svg | 9 +- web/assets/flags/ua.svg | 7 +- web/assets/flags/us.svg | 10 +- web/assets/flags/vn.svg | 12 +- web/assets/marktechpost.svg | 4 +- web/assets/providers/anthropic.svg | 2 +- web/assets/providers/deepseek.svg | 2 +- web/assets/providers/gemini.svg | 2 +- web/assets/providers/llamacpp.svg | 8 +- web/assets/providers/lmstudio.svg | 2 +- web/assets/providers/mistral.svg | 2 +- web/assets/providers/ollama.svg | 2 +- web/assets/providers/openai.svg | 2 +- web/assets/providers/openrouter.svg | 2 +- web/assets/providers/vllm.svg | 2 +- web/assets/providers/xai.svg | 2 +- 293 files changed, 293 insertions(+), 4286 deletions(-) diff --git a/src/chrome/icons/flags/bd.svg b/src/chrome/icons/flags/bd.svg index 16b794deb..8bafc11ef 100644 --- a/src/chrome/icons/flags/bd.svg +++ b/src/chrome/icons/flags/bd.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/br.svg b/src/chrome/icons/flags/br.svg index 719a763ca..e3eff4b36 100644 --- a/src/chrome/icons/flags/br.svg +++ b/src/chrome/icons/flags/br.svg @@ -1,45 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/cn.svg b/src/chrome/icons/flags/cn.svg index 10d3489a0..d4cab2ef4 100644 --- a/src/chrome/icons/flags/cn.svg +++ b/src/chrome/icons/flags/cn.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/de.svg b/src/chrome/icons/flags/de.svg index 71aa2d2c3..16a0041be 100644 --- a/src/chrome/icons/flags/de.svg +++ b/src/chrome/icons/flags/de.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/es.svg b/src/chrome/icons/flags/es.svg index a296ebf7e..7a7745913 100644 --- a/src/chrome/icons/flags/es.svg +++ b/src/chrome/icons/flags/es.svg @@ -1,544 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/fr.svg b/src/chrome/icons/flags/fr.svg index 4110e59e4..8f923f993 100644 --- a/src/chrome/icons/flags/fr.svg +++ b/src/chrome/icons/flags/fr.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/id.svg b/src/chrome/icons/flags/id.svg index 3b7c8fcfd..2d908da13 100644 --- a/src/chrome/icons/flags/id.svg +++ b/src/chrome/icons/flags/id.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/il.svg b/src/chrome/icons/flags/il.svg index f43be7e8e..bd779de9d 100644 --- a/src/chrome/icons/flags/il.svg +++ b/src/chrome/icons/flags/il.svg @@ -1,14 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/in.svg b/src/chrome/icons/flags/in.svg index bc47d7491..8c9991e8b 100644 --- a/src/chrome/icons/flags/in.svg +++ b/src/chrome/icons/flags/in.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/ir.svg b/src/chrome/icons/flags/ir.svg index 8c6d51621..606c4d39b 100644 --- a/src/chrome/icons/flags/ir.svg +++ b/src/chrome/icons/flags/ir.svg @@ -1,219 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/jp.svg b/src/chrome/icons/flags/jp.svg index cc1c181ce..b84407ff3 100644 --- a/src/chrome/icons/flags/jp.svg +++ b/src/chrome/icons/flags/jp.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/kr.svg b/src/chrome/icons/flags/kr.svg index 6947eab2b..6146fef35 100644 --- a/src/chrome/icons/flags/kr.svg +++ b/src/chrome/icons/flags/kr.svg @@ -1,24 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/my.svg b/src/chrome/icons/flags/my.svg index 115f864d8..c593a5516 100644 --- a/src/chrome/icons/flags/my.svg +++ b/src/chrome/icons/flags/my.svg @@ -1,26 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/nl.svg b/src/chrome/icons/flags/nl.svg index e90f5b035..cd4ee2704 100644 --- a/src/chrome/icons/flags/nl.svg +++ b/src/chrome/icons/flags/nl.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/ph.svg b/src/chrome/icons/flags/ph.svg index b910e24cd..1dcce8bf6 100644 --- a/src/chrome/icons/flags/ph.svg +++ b/src/chrome/icons/flags/ph.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/pl.svg b/src/chrome/icons/flags/pl.svg index 0fa514524..7a50bf2ce 100644 --- a/src/chrome/icons/flags/pl.svg +++ b/src/chrome/icons/flags/pl.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/ru.svg b/src/chrome/icons/flags/ru.svg index cf243011a..8ff50c9ba 100644 --- a/src/chrome/icons/flags/ru.svg +++ b/src/chrome/icons/flags/ru.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/sa.svg b/src/chrome/icons/flags/sa.svg index 596cf48bb..2f49621a0 100644 --- a/src/chrome/icons/flags/sa.svg +++ b/src/chrome/icons/flags/sa.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/th.svg b/src/chrome/icons/flags/th.svg index 1e93a61e9..1a441d1f7 100644 --- a/src/chrome/icons/flags/th.svg +++ b/src/chrome/icons/flags/th.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/tr.svg b/src/chrome/icons/flags/tr.svg index b96da21f0..ecd5fe5f6 100644 --- a/src/chrome/icons/flags/tr.svg +++ b/src/chrome/icons/flags/tr.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/ua.svg b/src/chrome/icons/flags/ua.svg index a339eb1b9..51eacf098 100644 --- a/src/chrome/icons/flags/ua.svg +++ b/src/chrome/icons/flags/ua.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/us.svg b/src/chrome/icons/flags/us.svg index 9cfd0c927..72895617b 100644 --- a/src/chrome/icons/flags/us.svg +++ b/src/chrome/icons/flags/us.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/flags/vn.svg b/src/chrome/icons/flags/vn.svg index 7e4bac8f4..69d6aa1f5 100644 --- a/src/chrome/icons/flags/vn.svg +++ b/src/chrome/icons/flags/vn.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/302ai.svg b/src/chrome/icons/providers/302ai.svg index 46f2e4315..95bbaf6b2 100644 --- a/src/chrome/icons/providers/302ai.svg +++ b/src/chrome/icons/providers/302ai.svg @@ -1,7 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/chrome/icons/providers/abacus.svg b/src/chrome/icons/providers/abacus.svg index 121a91d98..13c195518 100644 --- a/src/chrome/icons/providers/abacus.svg +++ b/src/chrome/icons/providers/abacus.svg @@ -1,14 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/aihubmix.svg b/src/chrome/icons/providers/aihubmix.svg index 33164b78b..8f3555623 100644 --- a/src/chrome/icons/providers/aihubmix.svg +++ b/src/chrome/icons/providers/aihubmix.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/alibaba-coding-plan-cn.svg b/src/chrome/icons/providers/alibaba-coding-plan-cn.svg index b3a2edc3c..95d098985 100644 --- a/src/chrome/icons/providers/alibaba-coding-plan-cn.svg +++ b/src/chrome/icons/providers/alibaba-coding-plan-cn.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/alibaba-coding-plan.svg b/src/chrome/icons/providers/alibaba-coding-plan.svg index b3a2edc3c..95d098985 100644 --- a/src/chrome/icons/providers/alibaba-coding-plan.svg +++ b/src/chrome/icons/providers/alibaba-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/alibaba.svg b/src/chrome/icons/providers/alibaba.svg index 33b3f645a..0fd14e021 100644 --- a/src/chrome/icons/providers/alibaba.svg +++ b/src/chrome/icons/providers/alibaba.svg @@ -1 +1 @@ -Qwen \ No newline at end of file +Qwen \ No newline at end of file diff --git a/src/chrome/icons/providers/anthropic.svg b/src/chrome/icons/providers/anthropic.svg index 5b81844cb..9aa4e4189 100644 --- a/src/chrome/icons/providers/anthropic.svg +++ b/src/chrome/icons/providers/anthropic.svg @@ -1 +1 @@ -Anthropic \ No newline at end of file +Anthropic \ No newline at end of file diff --git a/src/chrome/icons/providers/aws_bedrock.svg b/src/chrome/icons/providers/aws_bedrock.svg index e0f929a7a..8bb46335e 100644 --- a/src/chrome/icons/providers/aws_bedrock.svg +++ b/src/chrome/icons/providers/aws_bedrock.svg @@ -1 +1 @@ -Bedrock \ No newline at end of file +Bedrock \ No newline at end of file diff --git a/src/chrome/icons/providers/azure-cognitive-services.svg b/src/chrome/icons/providers/azure-cognitive-services.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/azure-cognitive-services.svg +++ b/src/chrome/icons/providers/azure-cognitive-services.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/azure_openai.svg b/src/chrome/icons/providers/azure_openai.svg index ed50209c1..208670160 100644 --- a/src/chrome/icons/providers/azure_openai.svg +++ b/src/chrome/icons/providers/azure_openai.svg @@ -1 +1 @@ -Azure \ No newline at end of file +Azure \ No newline at end of file diff --git a/src/chrome/icons/providers/bailing.svg b/src/chrome/icons/providers/bailing.svg index b8ed486a8..db65ca21b 100644 --- a/src/chrome/icons/providers/bailing.svg +++ b/src/chrome/icons/providers/bailing.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/baseten.svg b/src/chrome/icons/providers/baseten.svg index ffd4fbd8b..7ff53306f 100644 --- a/src/chrome/icons/providers/baseten.svg +++ b/src/chrome/icons/providers/baseten.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/berget.svg b/src/chrome/icons/providers/berget.svg index 831547a59..ccef3abe8 100644 --- a/src/chrome/icons/providers/berget.svg +++ b/src/chrome/icons/providers/berget.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/chrome/icons/providers/cerebras.svg b/src/chrome/icons/providers/cerebras.svg index b16759672..8c697cfc9 100644 --- a/src/chrome/icons/providers/cerebras.svg +++ b/src/chrome/icons/providers/cerebras.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/chutes.svg b/src/chrome/icons/providers/chutes.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/chutes.svg +++ b/src/chrome/icons/providers/chutes.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/clarifai.svg b/src/chrome/icons/providers/clarifai.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/clarifai.svg +++ b/src/chrome/icons/providers/clarifai.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/cloudferro-sherlock.svg b/src/chrome/icons/providers/cloudferro-sherlock.svg index 6f09a794e..8171da2f8 100644 --- a/src/chrome/icons/providers/cloudferro-sherlock.svg +++ b/src/chrome/icons/providers/cloudferro-sherlock.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/chrome/icons/providers/cloudflare.svg b/src/chrome/icons/providers/cloudflare.svg index d555b6f2c..3837faf2e 100644 --- a/src/chrome/icons/providers/cloudflare.svg +++ b/src/chrome/icons/providers/cloudflare.svg @@ -1 +1 @@ -Cloudflare \ No newline at end of file +Cloudflare \ No newline at end of file diff --git a/src/chrome/icons/providers/cohere.svg b/src/chrome/icons/providers/cohere.svg index cfeaa6002..5299b6a50 100644 --- a/src/chrome/icons/providers/cohere.svg +++ b/src/chrome/icons/providers/cohere.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/cortecs.svg b/src/chrome/icons/providers/cortecs.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/cortecs.svg +++ b/src/chrome/icons/providers/cortecs.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/deepinfra.svg b/src/chrome/icons/providers/deepinfra.svg index c35ab7183..4816e71f5 100644 --- a/src/chrome/icons/providers/deepinfra.svg +++ b/src/chrome/icons/providers/deepinfra.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/deepseek.svg b/src/chrome/icons/providers/deepseek.svg index 3fc230240..580f3edc8 100644 --- a/src/chrome/icons/providers/deepseek.svg +++ b/src/chrome/icons/providers/deepseek.svg @@ -1 +1 @@ -DeepSeek \ No newline at end of file +DeepSeek \ No newline at end of file diff --git a/src/chrome/icons/providers/digitalocean.svg b/src/chrome/icons/providers/digitalocean.svg index 5be390b9d..695ad1316 100644 --- a/src/chrome/icons/providers/digitalocean.svg +++ b/src/chrome/icons/providers/digitalocean.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/dinference.svg b/src/chrome/icons/providers/dinference.svg index e161327d4..ff785605a 100644 --- a/src/chrome/icons/providers/dinference.svg +++ b/src/chrome/icons/providers/dinference.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/chrome/icons/providers/drun.svg b/src/chrome/icons/providers/drun.svg index 472dee912..0e306be2b 100644 --- a/src/chrome/icons/providers/drun.svg +++ b/src/chrome/icons/providers/drun.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/evroc.svg b/src/chrome/icons/providers/evroc.svg index f65549485..e75c5382e 100644 --- a/src/chrome/icons/providers/evroc.svg +++ b/src/chrome/icons/providers/evroc.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/fastrouter.svg b/src/chrome/icons/providers/fastrouter.svg index ec73dc49c..6e5a45114 100644 --- a/src/chrome/icons/providers/fastrouter.svg +++ b/src/chrome/icons/providers/fastrouter.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/fireworks.svg b/src/chrome/icons/providers/fireworks.svg index a23445cf9..f7344285b 100644 --- a/src/chrome/icons/providers/fireworks.svg +++ b/src/chrome/icons/providers/fireworks.svg @@ -1 +1 @@ -Fireworks \ No newline at end of file +Fireworks \ No newline at end of file diff --git a/src/chrome/icons/providers/friendli.svg b/src/chrome/icons/providers/friendli.svg index 8acb7632d..1ea0ba0d4 100644 --- a/src/chrome/icons/providers/friendli.svg +++ b/src/chrome/icons/providers/friendli.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/gemini.svg b/src/chrome/icons/providers/gemini.svg index f1cf35757..33a088c19 100644 --- a/src/chrome/icons/providers/gemini.svg +++ b/src/chrome/icons/providers/gemini.svg @@ -1 +1 @@ -Gemini \ No newline at end of file +Gemini \ No newline at end of file diff --git a/src/chrome/icons/providers/google-vertex-anthropic.svg b/src/chrome/icons/providers/google-vertex-anthropic.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/google-vertex-anthropic.svg +++ b/src/chrome/icons/providers/google-vertex-anthropic.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/google-vertex.svg b/src/chrome/icons/providers/google-vertex.svg index fda56b447..165a4db84 100644 --- a/src/chrome/icons/providers/google-vertex.svg +++ b/src/chrome/icons/providers/google-vertex.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/gpt4all.svg b/src/chrome/icons/providers/gpt4all.svg index 2c5408798..6c11a6623 100644 --- a/src/chrome/icons/providers/gpt4all.svg +++ b/src/chrome/icons/providers/gpt4all.svg @@ -1,4 +1 @@ - - - G4A - +G4A \ No newline at end of file diff --git a/src/chrome/icons/providers/groq.svg b/src/chrome/icons/providers/groq.svg index 7294646b9..e24406845 100644 --- a/src/chrome/icons/providers/groq.svg +++ b/src/chrome/icons/providers/groq.svg @@ -1 +1 @@ -Groq \ No newline at end of file +Groq \ No newline at end of file diff --git a/src/chrome/icons/providers/helicone.svg b/src/chrome/icons/providers/helicone.svg index 8a4bd43ad..4f4a9e44f 100644 --- a/src/chrome/icons/providers/helicone.svg +++ b/src/chrome/icons/providers/helicone.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/huggingface.svg b/src/chrome/icons/providers/huggingface.svg index dc1cf3ffb..60511e938 100644 --- a/src/chrome/icons/providers/huggingface.svg +++ b/src/chrome/icons/providers/huggingface.svg @@ -1 +1 @@ -HuggingFace \ No newline at end of file +HuggingFace \ No newline at end of file diff --git a/src/chrome/icons/providers/iflowcn.svg b/src/chrome/icons/providers/iflowcn.svg index 6f35a7d59..61e21dc21 100644 --- a/src/chrome/icons/providers/iflowcn.svg +++ b/src/chrome/icons/providers/iflowcn.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/inception.svg b/src/chrome/icons/providers/inception.svg index f70ffbc78..005db9295 100644 --- a/src/chrome/icons/providers/inception.svg +++ b/src/chrome/icons/providers/inception.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/inference.svg b/src/chrome/icons/providers/inference.svg index c17f66574..b9f4f79e6 100644 --- a/src/chrome/icons/providers/inference.svg +++ b/src/chrome/icons/providers/inference.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/io-net.svg b/src/chrome/icons/providers/io-net.svg index 651e57df8..5938ad715 100644 --- a/src/chrome/icons/providers/io-net.svg +++ b/src/chrome/icons/providers/io-net.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/jiekou.svg b/src/chrome/icons/providers/jiekou.svg index 7fe6378e5..e70f8c415 100644 --- a/src/chrome/icons/providers/jiekou.svg +++ b/src/chrome/icons/providers/jiekou.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/kilo.svg b/src/chrome/icons/providers/kilo.svg index 0a761347a..02933bfd3 100644 --- a/src/chrome/icons/providers/kilo.svg +++ b/src/chrome/icons/providers/kilo.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/kimi-for-coding.svg b/src/chrome/icons/providers/kimi-for-coding.svg index 8f2af02e6..5955ba0fc 100644 --- a/src/chrome/icons/providers/kimi-for-coding.svg +++ b/src/chrome/icons/providers/kimi-for-coding.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/kimi.svg b/src/chrome/icons/providers/kimi.svg index ec5db53a4..cf8608426 100644 --- a/src/chrome/icons/providers/kimi.svg +++ b/src/chrome/icons/providers/kimi.svg @@ -1 +1 @@ -Kimi \ No newline at end of file +Kimi \ No newline at end of file diff --git a/src/chrome/icons/providers/kuae-cloud-coding-plan.svg b/src/chrome/icons/providers/kuae-cloud-coding-plan.svg index 3d0d0c455..ee2bd75f0 100644 --- a/src/chrome/icons/providers/kuae-cloud-coding-plan.svg +++ b/src/chrome/icons/providers/kuae-cloud-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/llama.svg b/src/chrome/icons/providers/llama.svg index 3053b251f..d46af7ceb 100644 --- a/src/chrome/icons/providers/llama.svg +++ b/src/chrome/icons/providers/llama.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/llamacpp.svg b/src/chrome/icons/providers/llamacpp.svg index 613e8da25..f6bbe19ec 100644 --- a/src/chrome/icons/providers/llamacpp.svg +++ b/src/chrome/icons/providers/llamacpp.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/lmstudio.svg b/src/chrome/icons/providers/lmstudio.svg index ea0816b63..ac29870af 100644 --- a/src/chrome/icons/providers/lmstudio.svg +++ b/src/chrome/icons/providers/lmstudio.svg @@ -1 +1 @@ -LM Studio \ No newline at end of file +LM Studio \ No newline at end of file diff --git a/src/chrome/icons/providers/local_openai_proxy.svg b/src/chrome/icons/providers/local_openai_proxy.svg index 309374ec1..92616889d 100644 --- a/src/chrome/icons/providers/local_openai_proxy.svg +++ b/src/chrome/icons/providers/local_openai_proxy.svg @@ -1,8 +1 @@ - - Local OpenAI-compatible Proxy - - - - - - +Local OpenAI-compatible Proxy \ No newline at end of file diff --git a/src/chrome/icons/providers/lucidquery.svg b/src/chrome/icons/providers/lucidquery.svg index 6420a042e..c573d50db 100644 --- a/src/chrome/icons/providers/lucidquery.svg +++ b/src/chrome/icons/providers/lucidquery.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/meganova.svg b/src/chrome/icons/providers/meganova.svg index 59e8b3a1f..7630d6705 100644 --- a/src/chrome/icons/providers/meganova.svg +++ b/src/chrome/icons/providers/meganova.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/minimax-cn-coding-plan.svg b/src/chrome/icons/providers/minimax-cn-coding-plan.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/minimax-cn-coding-plan.svg +++ b/src/chrome/icons/providers/minimax-cn-coding-plan.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/minimax-coding-plan.svg b/src/chrome/icons/providers/minimax-coding-plan.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/minimax-coding-plan.svg +++ b/src/chrome/icons/providers/minimax-coding-plan.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/minimax.svg b/src/chrome/icons/providers/minimax.svg index 2a60bd473..0a17db312 100644 --- a/src/chrome/icons/providers/minimax.svg +++ b/src/chrome/icons/providers/minimax.svg @@ -1 +1 @@ -Minimax \ No newline at end of file +Minimax \ No newline at end of file diff --git a/src/chrome/icons/providers/mistral.svg b/src/chrome/icons/providers/mistral.svg index 8e03e244b..8d4f2bb23 100644 --- a/src/chrome/icons/providers/mistral.svg +++ b/src/chrome/icons/providers/mistral.svg @@ -1 +1 @@ -Mistral \ No newline at end of file +Mistral \ No newline at end of file diff --git a/src/chrome/icons/providers/moark.svg b/src/chrome/icons/providers/moark.svg index dc84a9191..d903d64a9 100644 --- a/src/chrome/icons/providers/moark.svg +++ b/src/chrome/icons/providers/moark.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/modelscope.svg b/src/chrome/icons/providers/modelscope.svg index 94a894a55..66af60379 100644 --- a/src/chrome/icons/providers/modelscope.svg +++ b/src/chrome/icons/providers/modelscope.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/morph.svg b/src/chrome/icons/providers/morph.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/morph.svg +++ b/src/chrome/icons/providers/morph.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/nano-gpt.svg b/src/chrome/icons/providers/nano-gpt.svg index f7fbb4cdc..613c58ef3 100644 --- a/src/chrome/icons/providers/nano-gpt.svg +++ b/src/chrome/icons/providers/nano-gpt.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/chrome/icons/providers/nearai.svg b/src/chrome/icons/providers/nearai.svg index f69388bf8..c746e5505 100644 --- a/src/chrome/icons/providers/nearai.svg +++ b/src/chrome/icons/providers/nearai.svg @@ -1 +1 @@ -NEAR AI Cloud +NEAR AI Cloud \ No newline at end of file diff --git a/src/chrome/icons/providers/nebius.svg b/src/chrome/icons/providers/nebius.svg index aeba5890d..b6a322d07 100644 --- a/src/chrome/icons/providers/nebius.svg +++ b/src/chrome/icons/providers/nebius.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/nova.svg b/src/chrome/icons/providers/nova.svg index 9fcae228c..f16342791 100644 --- a/src/chrome/icons/providers/nova.svg +++ b/src/chrome/icons/providers/nova.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/novita-ai.svg b/src/chrome/icons/providers/novita-ai.svg index ac537b8dd..f58e70b1f 100644 --- a/src/chrome/icons/providers/novita-ai.svg +++ b/src/chrome/icons/providers/novita-ai.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/nvidia.svg b/src/chrome/icons/providers/nvidia.svg index a9683c2e0..9086facfa 100644 --- a/src/chrome/icons/providers/nvidia.svg +++ b/src/chrome/icons/providers/nvidia.svg @@ -1 +1 @@ -Nvidia \ No newline at end of file +Nvidia \ No newline at end of file diff --git a/src/chrome/icons/providers/ollama-cloud.svg b/src/chrome/icons/providers/ollama-cloud.svg index 08c05cf28..33c5654fb 100644 --- a/src/chrome/icons/providers/ollama-cloud.svg +++ b/src/chrome/icons/providers/ollama-cloud.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/ollama.svg b/src/chrome/icons/providers/ollama.svg index cc887e3dc..aebc3abf4 100644 --- a/src/chrome/icons/providers/ollama.svg +++ b/src/chrome/icons/providers/ollama.svg @@ -1 +1 @@ -Ollama \ No newline at end of file +Ollama \ No newline at end of file diff --git a/src/chrome/icons/providers/openai.svg b/src/chrome/icons/providers/openai.svg index 50d94d6c1..61342bd19 100644 --- a/src/chrome/icons/providers/openai.svg +++ b/src/chrome/icons/providers/openai.svg @@ -1 +1 @@ -OpenAI \ No newline at end of file +OpenAI \ No newline at end of file diff --git a/src/chrome/icons/providers/opencode-go.svg b/src/chrome/icons/providers/opencode-go.svg index e0833b923..3a6def94d 100644 --- a/src/chrome/icons/providers/opencode-go.svg +++ b/src/chrome/icons/providers/opencode-go.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/opencode.svg b/src/chrome/icons/providers/opencode.svg index 950846582..b28984442 100644 --- a/src/chrome/icons/providers/opencode.svg +++ b/src/chrome/icons/providers/opencode.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/openrouter.svg b/src/chrome/icons/providers/openrouter.svg index e6cca2a86..e51b58d3e 100644 --- a/src/chrome/icons/providers/openrouter.svg +++ b/src/chrome/icons/providers/openrouter.svg @@ -1 +1 @@ -OpenRouter \ No newline at end of file +OpenRouter \ No newline at end of file diff --git a/src/chrome/icons/providers/orcarouter.svg b/src/chrome/icons/providers/orcarouter.svg index 07f2b6fa2..922b7bcd0 100644 --- a/src/chrome/icons/providers/orcarouter.svg +++ b/src/chrome/icons/providers/orcarouter.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/ovhcloud.svg b/src/chrome/icons/providers/ovhcloud.svg index 064b0835b..8162a70a0 100644 --- a/src/chrome/icons/providers/ovhcloud.svg +++ b/src/chrome/icons/providers/ovhcloud.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/perplexity-agent.svg b/src/chrome/icons/providers/perplexity-agent.svg index a0f38862a..b657b3753 100644 --- a/src/chrome/icons/providers/perplexity-agent.svg +++ b/src/chrome/icons/providers/perplexity-agent.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/perplexity.svg b/src/chrome/icons/providers/perplexity.svg index a0f38862a..b657b3753 100644 --- a/src/chrome/icons/providers/perplexity.svg +++ b/src/chrome/icons/providers/perplexity.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/poe.svg b/src/chrome/icons/providers/poe.svg index a5ab62d72..306e0a6dc 100644 --- a/src/chrome/icons/providers/poe.svg +++ b/src/chrome/icons/providers/poe.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/pollinations.svg b/src/chrome/icons/providers/pollinations.svg index 893c7b714..bd37e8c03 100644 --- a/src/chrome/icons/providers/pollinations.svg +++ b/src/chrome/icons/providers/pollinations.svg @@ -1 +1 @@ -Pollinations AI +Pollinations AI \ No newline at end of file diff --git a/src/chrome/icons/providers/privatemode-ai.svg b/src/chrome/icons/providers/privatemode-ai.svg index edb5a6d76..6f268cd00 100644 --- a/src/chrome/icons/providers/privatemode-ai.svg +++ b/src/chrome/icons/providers/privatemode-ai.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/chrome/icons/providers/qihang-ai.svg b/src/chrome/icons/providers/qihang-ai.svg index 9e58c7440..fc9911abf 100644 --- a/src/chrome/icons/providers/qihang-ai.svg +++ b/src/chrome/icons/providers/qihang-ai.svg @@ -1,9 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/qiniu-ai.svg b/src/chrome/icons/providers/qiniu-ai.svg index 858560f9f..2bc874f31 100644 --- a/src/chrome/icons/providers/qiniu-ai.svg +++ b/src/chrome/icons/providers/qiniu-ai.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/requesty.svg b/src/chrome/icons/providers/requesty.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/requesty.svg +++ b/src/chrome/icons/providers/requesty.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/scaleway.svg b/src/chrome/icons/providers/scaleway.svg index 7574f72f3..a5cf1fe05 100644 --- a/src/chrome/icons/providers/scaleway.svg +++ b/src/chrome/icons/providers/scaleway.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/siliconflow-cn.svg b/src/chrome/icons/providers/siliconflow-cn.svg index 13cac22b9..ffe32c54e 100644 --- a/src/chrome/icons/providers/siliconflow-cn.svg +++ b/src/chrome/icons/providers/siliconflow-cn.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/siliconflow.svg b/src/chrome/icons/providers/siliconflow.svg index 13cac22b9..ffe32c54e 100644 --- a/src/chrome/icons/providers/siliconflow.svg +++ b/src/chrome/icons/providers/siliconflow.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/stackit.svg b/src/chrome/icons/providers/stackit.svg index 0d78b781a..ec062ecce 100644 --- a/src/chrome/icons/providers/stackit.svg +++ b/src/chrome/icons/providers/stackit.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/stepfun.svg b/src/chrome/icons/providers/stepfun.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/stepfun.svg +++ b/src/chrome/icons/providers/stepfun.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/submodel.svg b/src/chrome/icons/providers/submodel.svg index 5bef03c64..283348f25 100644 --- a/src/chrome/icons/providers/submodel.svg +++ b/src/chrome/icons/providers/submodel.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/synthetic.svg b/src/chrome/icons/providers/synthetic.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/synthetic.svg +++ b/src/chrome/icons/providers/synthetic.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/tencent-coding-plan.svg b/src/chrome/icons/providers/tencent-coding-plan.svg index 502e51a5b..522db4a81 100644 --- a/src/chrome/icons/providers/tencent-coding-plan.svg +++ b/src/chrome/icons/providers/tencent-coding-plan.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/together.svg b/src/chrome/icons/providers/together.svg index bb460ab5b..3f4c03e35 100644 --- a/src/chrome/icons/providers/together.svg +++ b/src/chrome/icons/providers/together.svg @@ -1 +1 @@ -together.ai \ No newline at end of file +together.ai \ No newline at end of file diff --git a/src/chrome/icons/providers/unsloth.svg b/src/chrome/icons/providers/unsloth.svg index 3bdcfd893..5e47fbb21 100644 --- a/src/chrome/icons/providers/unsloth.svg +++ b/src/chrome/icons/providers/unsloth.svg @@ -1,16 +1 @@ - -Unsloth - - - - - - - - - - - - - - +Unsloth \ No newline at end of file diff --git a/src/chrome/icons/providers/upstage.svg b/src/chrome/icons/providers/upstage.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/upstage.svg +++ b/src/chrome/icons/providers/upstage.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/v0.svg b/src/chrome/icons/providers/v0.svg index 09f3b411e..303d8f1ce 100644 --- a/src/chrome/icons/providers/v0.svg +++ b/src/chrome/icons/providers/v0.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/venice.svg b/src/chrome/icons/providers/venice.svg index 3d5809e3b..d214582a1 100644 --- a/src/chrome/icons/providers/venice.svg +++ b/src/chrome/icons/providers/venice.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/vercel.svg b/src/chrome/icons/providers/vercel.svg index a99425f2a..d08c1cff1 100644 --- a/src/chrome/icons/providers/vercel.svg +++ b/src/chrome/icons/providers/vercel.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/vivgrid.svg b/src/chrome/icons/providers/vivgrid.svg index 928fa3ff1..37c27d0d0 100644 --- a/src/chrome/icons/providers/vivgrid.svg +++ b/src/chrome/icons/providers/vivgrid.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/vllm.svg b/src/chrome/icons/providers/vllm.svg index 54acc3de2..ba278c758 100644 --- a/src/chrome/icons/providers/vllm.svg +++ b/src/chrome/icons/providers/vllm.svg @@ -1 +1 @@ -vLLM \ No newline at end of file +vLLM \ No newline at end of file diff --git a/src/chrome/icons/providers/vultr.svg b/src/chrome/icons/providers/vultr.svg index 9205d4484..3b5e25a73 100644 --- a/src/chrome/icons/providers/vultr.svg +++ b/src/chrome/icons/providers/vultr.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/wandb.svg b/src/chrome/icons/providers/wandb.svg index 086e9aa1f..ce1f77422 100644 --- a/src/chrome/icons/providers/wandb.svg +++ b/src/chrome/icons/providers/wandb.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/webgpu.svg b/src/chrome/icons/providers/webgpu.svg index a1946dba5..56c53934f 100644 --- a/src/chrome/icons/providers/webgpu.svg +++ b/src/chrome/icons/providers/webgpu.svg @@ -1,6 +1 @@ - - WebGPU in-browser provider - - - - +WebGPU in-browser provider \ No newline at end of file diff --git a/src/chrome/icons/providers/xai.svg b/src/chrome/icons/providers/xai.svg index 536e71390..d12e6ba00 100644 --- a/src/chrome/icons/providers/xai.svg +++ b/src/chrome/icons/providers/xai.svg @@ -1 +1 @@ -Grok \ No newline at end of file +Grok \ No newline at end of file diff --git a/src/chrome/icons/providers/xiaomi.svg b/src/chrome/icons/providers/xiaomi.svg index 4a893919e..a8f35bb27 100644 --- a/src/chrome/icons/providers/xiaomi.svg +++ b/src/chrome/icons/providers/xiaomi.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/z_ai.svg b/src/chrome/icons/providers/z_ai.svg index 4f511bd72..64cf6d27b 100644 --- a/src/chrome/icons/providers/z_ai.svg +++ b/src/chrome/icons/providers/z_ai.svg @@ -1,219 +1 @@ - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/zai-coding-plan.svg b/src/chrome/icons/providers/zai-coding-plan.svg index d7da9b7c5..5fd25266e 100644 --- a/src/chrome/icons/providers/zai-coding-plan.svg +++ b/src/chrome/icons/providers/zai-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/zenmux.svg b/src/chrome/icons/providers/zenmux.svg index 9eb8045e4..77203134e 100644 --- a/src/chrome/icons/providers/zenmux.svg +++ b/src/chrome/icons/providers/zenmux.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/zhipuai-coding-plan.svg b/src/chrome/icons/providers/zhipuai-coding-plan.svg index 3d0d0c455..ee2bd75f0 100644 --- a/src/chrome/icons/providers/zhipuai-coding-plan.svg +++ b/src/chrome/icons/providers/zhipuai-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/chrome/icons/providers/zhipuai.svg b/src/chrome/icons/providers/zhipuai.svg index d7da9b7c5..5fd25266e 100644 --- a/src/chrome/icons/providers/zhipuai.svg +++ b/src/chrome/icons/providers/zhipuai.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/bd.svg b/src/firefox/icons/flags/bd.svg index 16b794deb..8bafc11ef 100644 --- a/src/firefox/icons/flags/bd.svg +++ b/src/firefox/icons/flags/bd.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/br.svg b/src/firefox/icons/flags/br.svg index 719a763ca..e3eff4b36 100644 --- a/src/firefox/icons/flags/br.svg +++ b/src/firefox/icons/flags/br.svg @@ -1,45 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/cn.svg b/src/firefox/icons/flags/cn.svg index 10d3489a0..d4cab2ef4 100644 --- a/src/firefox/icons/flags/cn.svg +++ b/src/firefox/icons/flags/cn.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/de.svg b/src/firefox/icons/flags/de.svg index 71aa2d2c3..16a0041be 100644 --- a/src/firefox/icons/flags/de.svg +++ b/src/firefox/icons/flags/de.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/es.svg b/src/firefox/icons/flags/es.svg index a296ebf7e..7a7745913 100644 --- a/src/firefox/icons/flags/es.svg +++ b/src/firefox/icons/flags/es.svg @@ -1,544 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/fr.svg b/src/firefox/icons/flags/fr.svg index 4110e59e4..8f923f993 100644 --- a/src/firefox/icons/flags/fr.svg +++ b/src/firefox/icons/flags/fr.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/id.svg b/src/firefox/icons/flags/id.svg index 3b7c8fcfd..2d908da13 100644 --- a/src/firefox/icons/flags/id.svg +++ b/src/firefox/icons/flags/id.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/il.svg b/src/firefox/icons/flags/il.svg index f43be7e8e..bd779de9d 100644 --- a/src/firefox/icons/flags/il.svg +++ b/src/firefox/icons/flags/il.svg @@ -1,14 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/in.svg b/src/firefox/icons/flags/in.svg index bc47d7491..8c9991e8b 100644 --- a/src/firefox/icons/flags/in.svg +++ b/src/firefox/icons/flags/in.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/ir.svg b/src/firefox/icons/flags/ir.svg index 8c6d51621..606c4d39b 100644 --- a/src/firefox/icons/flags/ir.svg +++ b/src/firefox/icons/flags/ir.svg @@ -1,219 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/jp.svg b/src/firefox/icons/flags/jp.svg index cc1c181ce..b84407ff3 100644 --- a/src/firefox/icons/flags/jp.svg +++ b/src/firefox/icons/flags/jp.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/kr.svg b/src/firefox/icons/flags/kr.svg index 6947eab2b..6146fef35 100644 --- a/src/firefox/icons/flags/kr.svg +++ b/src/firefox/icons/flags/kr.svg @@ -1,24 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/my.svg b/src/firefox/icons/flags/my.svg index 115f864d8..c593a5516 100644 --- a/src/firefox/icons/flags/my.svg +++ b/src/firefox/icons/flags/my.svg @@ -1,26 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/nl.svg b/src/firefox/icons/flags/nl.svg index e90f5b035..cd4ee2704 100644 --- a/src/firefox/icons/flags/nl.svg +++ b/src/firefox/icons/flags/nl.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/ph.svg b/src/firefox/icons/flags/ph.svg index b910e24cd..1dcce8bf6 100644 --- a/src/firefox/icons/flags/ph.svg +++ b/src/firefox/icons/flags/ph.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/pl.svg b/src/firefox/icons/flags/pl.svg index 0fa514524..7a50bf2ce 100644 --- a/src/firefox/icons/flags/pl.svg +++ b/src/firefox/icons/flags/pl.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/ru.svg b/src/firefox/icons/flags/ru.svg index cf243011a..8ff50c9ba 100644 --- a/src/firefox/icons/flags/ru.svg +++ b/src/firefox/icons/flags/ru.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/sa.svg b/src/firefox/icons/flags/sa.svg index 596cf48bb..2f49621a0 100644 --- a/src/firefox/icons/flags/sa.svg +++ b/src/firefox/icons/flags/sa.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/th.svg b/src/firefox/icons/flags/th.svg index 1e93a61e9..1a441d1f7 100644 --- a/src/firefox/icons/flags/th.svg +++ b/src/firefox/icons/flags/th.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/tr.svg b/src/firefox/icons/flags/tr.svg index b96da21f0..ecd5fe5f6 100644 --- a/src/firefox/icons/flags/tr.svg +++ b/src/firefox/icons/flags/tr.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/ua.svg b/src/firefox/icons/flags/ua.svg index a339eb1b9..51eacf098 100644 --- a/src/firefox/icons/flags/ua.svg +++ b/src/firefox/icons/flags/ua.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/us.svg b/src/firefox/icons/flags/us.svg index 9cfd0c927..72895617b 100644 --- a/src/firefox/icons/flags/us.svg +++ b/src/firefox/icons/flags/us.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/flags/vn.svg b/src/firefox/icons/flags/vn.svg index 7e4bac8f4..69d6aa1f5 100644 --- a/src/firefox/icons/flags/vn.svg +++ b/src/firefox/icons/flags/vn.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/302ai.svg b/src/firefox/icons/providers/302ai.svg index 46f2e4315..95bbaf6b2 100644 --- a/src/firefox/icons/providers/302ai.svg +++ b/src/firefox/icons/providers/302ai.svg @@ -1,7 +1 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/firefox/icons/providers/abacus.svg b/src/firefox/icons/providers/abacus.svg index 121a91d98..13c195518 100644 --- a/src/firefox/icons/providers/abacus.svg +++ b/src/firefox/icons/providers/abacus.svg @@ -1,14 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/aihubmix.svg b/src/firefox/icons/providers/aihubmix.svg index 33164b78b..8f3555623 100644 --- a/src/firefox/icons/providers/aihubmix.svg +++ b/src/firefox/icons/providers/aihubmix.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/alibaba-coding-plan-cn.svg b/src/firefox/icons/providers/alibaba-coding-plan-cn.svg index b3a2edc3c..95d098985 100644 --- a/src/firefox/icons/providers/alibaba-coding-plan-cn.svg +++ b/src/firefox/icons/providers/alibaba-coding-plan-cn.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/alibaba-coding-plan.svg b/src/firefox/icons/providers/alibaba-coding-plan.svg index b3a2edc3c..95d098985 100644 --- a/src/firefox/icons/providers/alibaba-coding-plan.svg +++ b/src/firefox/icons/providers/alibaba-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/alibaba.svg b/src/firefox/icons/providers/alibaba.svg index 33b3f645a..0fd14e021 100644 --- a/src/firefox/icons/providers/alibaba.svg +++ b/src/firefox/icons/providers/alibaba.svg @@ -1 +1 @@ -Qwen \ No newline at end of file +Qwen \ No newline at end of file diff --git a/src/firefox/icons/providers/anthropic.svg b/src/firefox/icons/providers/anthropic.svg index 5b81844cb..9aa4e4189 100644 --- a/src/firefox/icons/providers/anthropic.svg +++ b/src/firefox/icons/providers/anthropic.svg @@ -1 +1 @@ -Anthropic \ No newline at end of file +Anthropic \ No newline at end of file diff --git a/src/firefox/icons/providers/aws_bedrock.svg b/src/firefox/icons/providers/aws_bedrock.svg index e0f929a7a..8bb46335e 100644 --- a/src/firefox/icons/providers/aws_bedrock.svg +++ b/src/firefox/icons/providers/aws_bedrock.svg @@ -1 +1 @@ -Bedrock \ No newline at end of file +Bedrock \ No newline at end of file diff --git a/src/firefox/icons/providers/azure-cognitive-services.svg b/src/firefox/icons/providers/azure-cognitive-services.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/azure-cognitive-services.svg +++ b/src/firefox/icons/providers/azure-cognitive-services.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/azure_openai.svg b/src/firefox/icons/providers/azure_openai.svg index ed50209c1..208670160 100644 --- a/src/firefox/icons/providers/azure_openai.svg +++ b/src/firefox/icons/providers/azure_openai.svg @@ -1 +1 @@ -Azure \ No newline at end of file +Azure \ No newline at end of file diff --git a/src/firefox/icons/providers/bailing.svg b/src/firefox/icons/providers/bailing.svg index b8ed486a8..db65ca21b 100644 --- a/src/firefox/icons/providers/bailing.svg +++ b/src/firefox/icons/providers/bailing.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/baseten.svg b/src/firefox/icons/providers/baseten.svg index ffd4fbd8b..7ff53306f 100644 --- a/src/firefox/icons/providers/baseten.svg +++ b/src/firefox/icons/providers/baseten.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/berget.svg b/src/firefox/icons/providers/berget.svg index 831547a59..ccef3abe8 100644 --- a/src/firefox/icons/providers/berget.svg +++ b/src/firefox/icons/providers/berget.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/firefox/icons/providers/cerebras.svg b/src/firefox/icons/providers/cerebras.svg index b16759672..8c697cfc9 100644 --- a/src/firefox/icons/providers/cerebras.svg +++ b/src/firefox/icons/providers/cerebras.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/chutes.svg b/src/firefox/icons/providers/chutes.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/chutes.svg +++ b/src/firefox/icons/providers/chutes.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/clarifai.svg b/src/firefox/icons/providers/clarifai.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/clarifai.svg +++ b/src/firefox/icons/providers/clarifai.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/cloudferro-sherlock.svg b/src/firefox/icons/providers/cloudferro-sherlock.svg index 6f09a794e..8171da2f8 100644 --- a/src/firefox/icons/providers/cloudferro-sherlock.svg +++ b/src/firefox/icons/providers/cloudferro-sherlock.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/firefox/icons/providers/cloudflare.svg b/src/firefox/icons/providers/cloudflare.svg index d555b6f2c..3837faf2e 100644 --- a/src/firefox/icons/providers/cloudflare.svg +++ b/src/firefox/icons/providers/cloudflare.svg @@ -1 +1 @@ -Cloudflare \ No newline at end of file +Cloudflare \ No newline at end of file diff --git a/src/firefox/icons/providers/cohere.svg b/src/firefox/icons/providers/cohere.svg index cfeaa6002..5299b6a50 100644 --- a/src/firefox/icons/providers/cohere.svg +++ b/src/firefox/icons/providers/cohere.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/cortecs.svg b/src/firefox/icons/providers/cortecs.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/cortecs.svg +++ b/src/firefox/icons/providers/cortecs.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/deepinfra.svg b/src/firefox/icons/providers/deepinfra.svg index c35ab7183..4816e71f5 100644 --- a/src/firefox/icons/providers/deepinfra.svg +++ b/src/firefox/icons/providers/deepinfra.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/deepseek.svg b/src/firefox/icons/providers/deepseek.svg index 3fc230240..580f3edc8 100644 --- a/src/firefox/icons/providers/deepseek.svg +++ b/src/firefox/icons/providers/deepseek.svg @@ -1 +1 @@ -DeepSeek \ No newline at end of file +DeepSeek \ No newline at end of file diff --git a/src/firefox/icons/providers/digitalocean.svg b/src/firefox/icons/providers/digitalocean.svg index 5be390b9d..695ad1316 100644 --- a/src/firefox/icons/providers/digitalocean.svg +++ b/src/firefox/icons/providers/digitalocean.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/dinference.svg b/src/firefox/icons/providers/dinference.svg index e161327d4..ff785605a 100644 --- a/src/firefox/icons/providers/dinference.svg +++ b/src/firefox/icons/providers/dinference.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/firefox/icons/providers/drun.svg b/src/firefox/icons/providers/drun.svg index 472dee912..0e306be2b 100644 --- a/src/firefox/icons/providers/drun.svg +++ b/src/firefox/icons/providers/drun.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/evroc.svg b/src/firefox/icons/providers/evroc.svg index f65549485..e75c5382e 100644 --- a/src/firefox/icons/providers/evroc.svg +++ b/src/firefox/icons/providers/evroc.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/fastrouter.svg b/src/firefox/icons/providers/fastrouter.svg index ec73dc49c..6e5a45114 100644 --- a/src/firefox/icons/providers/fastrouter.svg +++ b/src/firefox/icons/providers/fastrouter.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/fireworks.svg b/src/firefox/icons/providers/fireworks.svg index a23445cf9..f7344285b 100644 --- a/src/firefox/icons/providers/fireworks.svg +++ b/src/firefox/icons/providers/fireworks.svg @@ -1 +1 @@ -Fireworks \ No newline at end of file +Fireworks \ No newline at end of file diff --git a/src/firefox/icons/providers/friendli.svg b/src/firefox/icons/providers/friendli.svg index 8acb7632d..1ea0ba0d4 100644 --- a/src/firefox/icons/providers/friendli.svg +++ b/src/firefox/icons/providers/friendli.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/gemini.svg b/src/firefox/icons/providers/gemini.svg index f1cf35757..33a088c19 100644 --- a/src/firefox/icons/providers/gemini.svg +++ b/src/firefox/icons/providers/gemini.svg @@ -1 +1 @@ -Gemini \ No newline at end of file +Gemini \ No newline at end of file diff --git a/src/firefox/icons/providers/google-vertex-anthropic.svg b/src/firefox/icons/providers/google-vertex-anthropic.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/google-vertex-anthropic.svg +++ b/src/firefox/icons/providers/google-vertex-anthropic.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/google-vertex.svg b/src/firefox/icons/providers/google-vertex.svg index fda56b447..165a4db84 100644 --- a/src/firefox/icons/providers/google-vertex.svg +++ b/src/firefox/icons/providers/google-vertex.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/gpt4all.svg b/src/firefox/icons/providers/gpt4all.svg index 2c5408798..6c11a6623 100644 --- a/src/firefox/icons/providers/gpt4all.svg +++ b/src/firefox/icons/providers/gpt4all.svg @@ -1,4 +1 @@ - - - G4A - +G4A \ No newline at end of file diff --git a/src/firefox/icons/providers/groq.svg b/src/firefox/icons/providers/groq.svg index 7294646b9..e24406845 100644 --- a/src/firefox/icons/providers/groq.svg +++ b/src/firefox/icons/providers/groq.svg @@ -1 +1 @@ -Groq \ No newline at end of file +Groq \ No newline at end of file diff --git a/src/firefox/icons/providers/helicone.svg b/src/firefox/icons/providers/helicone.svg index 8a4bd43ad..4f4a9e44f 100644 --- a/src/firefox/icons/providers/helicone.svg +++ b/src/firefox/icons/providers/helicone.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/huggingface.svg b/src/firefox/icons/providers/huggingface.svg index dc1cf3ffb..60511e938 100644 --- a/src/firefox/icons/providers/huggingface.svg +++ b/src/firefox/icons/providers/huggingface.svg @@ -1 +1 @@ -HuggingFace \ No newline at end of file +HuggingFace \ No newline at end of file diff --git a/src/firefox/icons/providers/iflowcn.svg b/src/firefox/icons/providers/iflowcn.svg index 6f35a7d59..61e21dc21 100644 --- a/src/firefox/icons/providers/iflowcn.svg +++ b/src/firefox/icons/providers/iflowcn.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/inception.svg b/src/firefox/icons/providers/inception.svg index f70ffbc78..005db9295 100644 --- a/src/firefox/icons/providers/inception.svg +++ b/src/firefox/icons/providers/inception.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/inference.svg b/src/firefox/icons/providers/inference.svg index c17f66574..b9f4f79e6 100644 --- a/src/firefox/icons/providers/inference.svg +++ b/src/firefox/icons/providers/inference.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/io-net.svg b/src/firefox/icons/providers/io-net.svg index 651e57df8..5938ad715 100644 --- a/src/firefox/icons/providers/io-net.svg +++ b/src/firefox/icons/providers/io-net.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/jiekou.svg b/src/firefox/icons/providers/jiekou.svg index 7fe6378e5..e70f8c415 100644 --- a/src/firefox/icons/providers/jiekou.svg +++ b/src/firefox/icons/providers/jiekou.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/kilo.svg b/src/firefox/icons/providers/kilo.svg index 0a761347a..02933bfd3 100644 --- a/src/firefox/icons/providers/kilo.svg +++ b/src/firefox/icons/providers/kilo.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/kimi-for-coding.svg b/src/firefox/icons/providers/kimi-for-coding.svg index 8f2af02e6..5955ba0fc 100644 --- a/src/firefox/icons/providers/kimi-for-coding.svg +++ b/src/firefox/icons/providers/kimi-for-coding.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/kimi.svg b/src/firefox/icons/providers/kimi.svg index ec5db53a4..cf8608426 100644 --- a/src/firefox/icons/providers/kimi.svg +++ b/src/firefox/icons/providers/kimi.svg @@ -1 +1 @@ -Kimi \ No newline at end of file +Kimi \ No newline at end of file diff --git a/src/firefox/icons/providers/kuae-cloud-coding-plan.svg b/src/firefox/icons/providers/kuae-cloud-coding-plan.svg index 3d0d0c455..ee2bd75f0 100644 --- a/src/firefox/icons/providers/kuae-cloud-coding-plan.svg +++ b/src/firefox/icons/providers/kuae-cloud-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/llama.svg b/src/firefox/icons/providers/llama.svg index 3053b251f..d46af7ceb 100644 --- a/src/firefox/icons/providers/llama.svg +++ b/src/firefox/icons/providers/llama.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/llamacpp.svg b/src/firefox/icons/providers/llamacpp.svg index 613e8da25..f6bbe19ec 100644 --- a/src/firefox/icons/providers/llamacpp.svg +++ b/src/firefox/icons/providers/llamacpp.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/lmstudio.svg b/src/firefox/icons/providers/lmstudio.svg index ea0816b63..ac29870af 100644 --- a/src/firefox/icons/providers/lmstudio.svg +++ b/src/firefox/icons/providers/lmstudio.svg @@ -1 +1 @@ -LM Studio \ No newline at end of file +LM Studio \ No newline at end of file diff --git a/src/firefox/icons/providers/local_openai_proxy.svg b/src/firefox/icons/providers/local_openai_proxy.svg index 309374ec1..92616889d 100644 --- a/src/firefox/icons/providers/local_openai_proxy.svg +++ b/src/firefox/icons/providers/local_openai_proxy.svg @@ -1,8 +1 @@ - - Local OpenAI-compatible Proxy - - - - - - +Local OpenAI-compatible Proxy \ No newline at end of file diff --git a/src/firefox/icons/providers/lucidquery.svg b/src/firefox/icons/providers/lucidquery.svg index 6420a042e..c573d50db 100644 --- a/src/firefox/icons/providers/lucidquery.svg +++ b/src/firefox/icons/providers/lucidquery.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/meganova.svg b/src/firefox/icons/providers/meganova.svg index 59e8b3a1f..7630d6705 100644 --- a/src/firefox/icons/providers/meganova.svg +++ b/src/firefox/icons/providers/meganova.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/minimax-cn-coding-plan.svg b/src/firefox/icons/providers/minimax-cn-coding-plan.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/minimax-cn-coding-plan.svg +++ b/src/firefox/icons/providers/minimax-cn-coding-plan.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/minimax-coding-plan.svg b/src/firefox/icons/providers/minimax-coding-plan.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/minimax-coding-plan.svg +++ b/src/firefox/icons/providers/minimax-coding-plan.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/minimax.svg b/src/firefox/icons/providers/minimax.svg index 2a60bd473..0a17db312 100644 --- a/src/firefox/icons/providers/minimax.svg +++ b/src/firefox/icons/providers/minimax.svg @@ -1 +1 @@ -Minimax \ No newline at end of file +Minimax \ No newline at end of file diff --git a/src/firefox/icons/providers/mistral.svg b/src/firefox/icons/providers/mistral.svg index 8e03e244b..8d4f2bb23 100644 --- a/src/firefox/icons/providers/mistral.svg +++ b/src/firefox/icons/providers/mistral.svg @@ -1 +1 @@ -Mistral \ No newline at end of file +Mistral \ No newline at end of file diff --git a/src/firefox/icons/providers/moark.svg b/src/firefox/icons/providers/moark.svg index dc84a9191..d903d64a9 100644 --- a/src/firefox/icons/providers/moark.svg +++ b/src/firefox/icons/providers/moark.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/modelscope.svg b/src/firefox/icons/providers/modelscope.svg index 94a894a55..66af60379 100644 --- a/src/firefox/icons/providers/modelscope.svg +++ b/src/firefox/icons/providers/modelscope.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/morph.svg b/src/firefox/icons/providers/morph.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/morph.svg +++ b/src/firefox/icons/providers/morph.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/nano-gpt.svg b/src/firefox/icons/providers/nano-gpt.svg index f7fbb4cdc..613c58ef3 100644 --- a/src/firefox/icons/providers/nano-gpt.svg +++ b/src/firefox/icons/providers/nano-gpt.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/firefox/icons/providers/nearai.svg b/src/firefox/icons/providers/nearai.svg index f69388bf8..c746e5505 100644 --- a/src/firefox/icons/providers/nearai.svg +++ b/src/firefox/icons/providers/nearai.svg @@ -1 +1 @@ -NEAR AI Cloud +NEAR AI Cloud \ No newline at end of file diff --git a/src/firefox/icons/providers/nebius.svg b/src/firefox/icons/providers/nebius.svg index aeba5890d..b6a322d07 100644 --- a/src/firefox/icons/providers/nebius.svg +++ b/src/firefox/icons/providers/nebius.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/nova.svg b/src/firefox/icons/providers/nova.svg index 9fcae228c..f16342791 100644 --- a/src/firefox/icons/providers/nova.svg +++ b/src/firefox/icons/providers/nova.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/novita-ai.svg b/src/firefox/icons/providers/novita-ai.svg index ac537b8dd..f58e70b1f 100644 --- a/src/firefox/icons/providers/novita-ai.svg +++ b/src/firefox/icons/providers/novita-ai.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/nvidia.svg b/src/firefox/icons/providers/nvidia.svg index a9683c2e0..9086facfa 100644 --- a/src/firefox/icons/providers/nvidia.svg +++ b/src/firefox/icons/providers/nvidia.svg @@ -1 +1 @@ -Nvidia \ No newline at end of file +Nvidia \ No newline at end of file diff --git a/src/firefox/icons/providers/ollama-cloud.svg b/src/firefox/icons/providers/ollama-cloud.svg index 08c05cf28..33c5654fb 100644 --- a/src/firefox/icons/providers/ollama-cloud.svg +++ b/src/firefox/icons/providers/ollama-cloud.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/ollama.svg b/src/firefox/icons/providers/ollama.svg index cc887e3dc..aebc3abf4 100644 --- a/src/firefox/icons/providers/ollama.svg +++ b/src/firefox/icons/providers/ollama.svg @@ -1 +1 @@ -Ollama \ No newline at end of file +Ollama \ No newline at end of file diff --git a/src/firefox/icons/providers/openai.svg b/src/firefox/icons/providers/openai.svg index 50d94d6c1..61342bd19 100644 --- a/src/firefox/icons/providers/openai.svg +++ b/src/firefox/icons/providers/openai.svg @@ -1 +1 @@ -OpenAI \ No newline at end of file +OpenAI \ No newline at end of file diff --git a/src/firefox/icons/providers/opencode-go.svg b/src/firefox/icons/providers/opencode-go.svg index e0833b923..3a6def94d 100644 --- a/src/firefox/icons/providers/opencode-go.svg +++ b/src/firefox/icons/providers/opencode-go.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/opencode.svg b/src/firefox/icons/providers/opencode.svg index 950846582..b28984442 100644 --- a/src/firefox/icons/providers/opencode.svg +++ b/src/firefox/icons/providers/opencode.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/openrouter.svg b/src/firefox/icons/providers/openrouter.svg index e6cca2a86..e51b58d3e 100644 --- a/src/firefox/icons/providers/openrouter.svg +++ b/src/firefox/icons/providers/openrouter.svg @@ -1 +1 @@ -OpenRouter \ No newline at end of file +OpenRouter \ No newline at end of file diff --git a/src/firefox/icons/providers/orcarouter.svg b/src/firefox/icons/providers/orcarouter.svg index 07f2b6fa2..922b7bcd0 100644 --- a/src/firefox/icons/providers/orcarouter.svg +++ b/src/firefox/icons/providers/orcarouter.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/ovhcloud.svg b/src/firefox/icons/providers/ovhcloud.svg index 064b0835b..8162a70a0 100644 --- a/src/firefox/icons/providers/ovhcloud.svg +++ b/src/firefox/icons/providers/ovhcloud.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/perplexity-agent.svg b/src/firefox/icons/providers/perplexity-agent.svg index a0f38862a..b657b3753 100644 --- a/src/firefox/icons/providers/perplexity-agent.svg +++ b/src/firefox/icons/providers/perplexity-agent.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/perplexity.svg b/src/firefox/icons/providers/perplexity.svg index a0f38862a..b657b3753 100644 --- a/src/firefox/icons/providers/perplexity.svg +++ b/src/firefox/icons/providers/perplexity.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/poe.svg b/src/firefox/icons/providers/poe.svg index a5ab62d72..306e0a6dc 100644 --- a/src/firefox/icons/providers/poe.svg +++ b/src/firefox/icons/providers/poe.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/pollinations.svg b/src/firefox/icons/providers/pollinations.svg index 893c7b714..bd37e8c03 100644 --- a/src/firefox/icons/providers/pollinations.svg +++ b/src/firefox/icons/providers/pollinations.svg @@ -1 +1 @@ -Pollinations AI +Pollinations AI \ No newline at end of file diff --git a/src/firefox/icons/providers/privatemode-ai.svg b/src/firefox/icons/providers/privatemode-ai.svg index edb5a6d76..6f268cd00 100644 --- a/src/firefox/icons/providers/privatemode-ai.svg +++ b/src/firefox/icons/providers/privatemode-ai.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/firefox/icons/providers/qihang-ai.svg b/src/firefox/icons/providers/qihang-ai.svg index 9e58c7440..fc9911abf 100644 --- a/src/firefox/icons/providers/qihang-ai.svg +++ b/src/firefox/icons/providers/qihang-ai.svg @@ -1,9 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/qiniu-ai.svg b/src/firefox/icons/providers/qiniu-ai.svg index 858560f9f..2bc874f31 100644 --- a/src/firefox/icons/providers/qiniu-ai.svg +++ b/src/firefox/icons/providers/qiniu-ai.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/requesty.svg b/src/firefox/icons/providers/requesty.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/requesty.svg +++ b/src/firefox/icons/providers/requesty.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/scaleway.svg b/src/firefox/icons/providers/scaleway.svg index 7574f72f3..a5cf1fe05 100644 --- a/src/firefox/icons/providers/scaleway.svg +++ b/src/firefox/icons/providers/scaleway.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/siliconflow-cn.svg b/src/firefox/icons/providers/siliconflow-cn.svg index 13cac22b9..ffe32c54e 100644 --- a/src/firefox/icons/providers/siliconflow-cn.svg +++ b/src/firefox/icons/providers/siliconflow-cn.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/siliconflow.svg b/src/firefox/icons/providers/siliconflow.svg index 13cac22b9..ffe32c54e 100644 --- a/src/firefox/icons/providers/siliconflow.svg +++ b/src/firefox/icons/providers/siliconflow.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/stackit.svg b/src/firefox/icons/providers/stackit.svg index 0d78b781a..ec062ecce 100644 --- a/src/firefox/icons/providers/stackit.svg +++ b/src/firefox/icons/providers/stackit.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/stepfun.svg b/src/firefox/icons/providers/stepfun.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/stepfun.svg +++ b/src/firefox/icons/providers/stepfun.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/submodel.svg b/src/firefox/icons/providers/submodel.svg index 5bef03c64..283348f25 100644 --- a/src/firefox/icons/providers/submodel.svg +++ b/src/firefox/icons/providers/submodel.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/synthetic.svg b/src/firefox/icons/providers/synthetic.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/synthetic.svg +++ b/src/firefox/icons/providers/synthetic.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/tencent-coding-plan.svg b/src/firefox/icons/providers/tencent-coding-plan.svg index 502e51a5b..522db4a81 100644 --- a/src/firefox/icons/providers/tencent-coding-plan.svg +++ b/src/firefox/icons/providers/tencent-coding-plan.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/together.svg b/src/firefox/icons/providers/together.svg index bb460ab5b..3f4c03e35 100644 --- a/src/firefox/icons/providers/together.svg +++ b/src/firefox/icons/providers/together.svg @@ -1 +1 @@ -together.ai \ No newline at end of file +together.ai \ No newline at end of file diff --git a/src/firefox/icons/providers/unsloth.svg b/src/firefox/icons/providers/unsloth.svg index 3bdcfd893..5e47fbb21 100644 --- a/src/firefox/icons/providers/unsloth.svg +++ b/src/firefox/icons/providers/unsloth.svg @@ -1,16 +1 @@ - -Unsloth - - - - - - - - - - - - - - +Unsloth \ No newline at end of file diff --git a/src/firefox/icons/providers/upstage.svg b/src/firefox/icons/providers/upstage.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/upstage.svg +++ b/src/firefox/icons/providers/upstage.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/v0.svg b/src/firefox/icons/providers/v0.svg index 09f3b411e..303d8f1ce 100644 --- a/src/firefox/icons/providers/v0.svg +++ b/src/firefox/icons/providers/v0.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/venice.svg b/src/firefox/icons/providers/venice.svg index 3d5809e3b..d214582a1 100644 --- a/src/firefox/icons/providers/venice.svg +++ b/src/firefox/icons/providers/venice.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/vercel.svg b/src/firefox/icons/providers/vercel.svg index a99425f2a..d08c1cff1 100644 --- a/src/firefox/icons/providers/vercel.svg +++ b/src/firefox/icons/providers/vercel.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/vivgrid.svg b/src/firefox/icons/providers/vivgrid.svg index 928fa3ff1..37c27d0d0 100644 --- a/src/firefox/icons/providers/vivgrid.svg +++ b/src/firefox/icons/providers/vivgrid.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/vllm.svg b/src/firefox/icons/providers/vllm.svg index 54acc3de2..ba278c758 100644 --- a/src/firefox/icons/providers/vllm.svg +++ b/src/firefox/icons/providers/vllm.svg @@ -1 +1 @@ -vLLM \ No newline at end of file +vLLM \ No newline at end of file diff --git a/src/firefox/icons/providers/vultr.svg b/src/firefox/icons/providers/vultr.svg index 9205d4484..3b5e25a73 100644 --- a/src/firefox/icons/providers/vultr.svg +++ b/src/firefox/icons/providers/vultr.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/wandb.svg b/src/firefox/icons/providers/wandb.svg index 086e9aa1f..ce1f77422 100644 --- a/src/firefox/icons/providers/wandb.svg +++ b/src/firefox/icons/providers/wandb.svg @@ -1,24 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/xai.svg b/src/firefox/icons/providers/xai.svg index 536e71390..d12e6ba00 100644 --- a/src/firefox/icons/providers/xai.svg +++ b/src/firefox/icons/providers/xai.svg @@ -1 +1 @@ -Grok \ No newline at end of file +Grok \ No newline at end of file diff --git a/src/firefox/icons/providers/xiaomi.svg b/src/firefox/icons/providers/xiaomi.svg index 4a893919e..a8f35bb27 100644 --- a/src/firefox/icons/providers/xiaomi.svg +++ b/src/firefox/icons/providers/xiaomi.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/z_ai.svg b/src/firefox/icons/providers/z_ai.svg index 4f511bd72..64cf6d27b 100644 --- a/src/firefox/icons/providers/z_ai.svg +++ b/src/firefox/icons/providers/z_ai.svg @@ -1,219 +1 @@ - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/zai-coding-plan.svg b/src/firefox/icons/providers/zai-coding-plan.svg index d7da9b7c5..5fd25266e 100644 --- a/src/firefox/icons/providers/zai-coding-plan.svg +++ b/src/firefox/icons/providers/zai-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/zenmux.svg b/src/firefox/icons/providers/zenmux.svg index 9eb8045e4..77203134e 100644 --- a/src/firefox/icons/providers/zenmux.svg +++ b/src/firefox/icons/providers/zenmux.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/zhipuai-coding-plan.svg b/src/firefox/icons/providers/zhipuai-coding-plan.svg index 3d0d0c455..ee2bd75f0 100644 --- a/src/firefox/icons/providers/zhipuai-coding-plan.svg +++ b/src/firefox/icons/providers/zhipuai-coding-plan.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/src/firefox/icons/providers/zhipuai.svg b/src/firefox/icons/providers/zhipuai.svg index d7da9b7c5..5fd25266e 100644 --- a/src/firefox/icons/providers/zhipuai.svg +++ b/src/firefox/icons/providers/zhipuai.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/web/assets/browser-chrome.svg b/web/assets/browser-chrome.svg index b12aba9a1..a6464273f 100644 --- a/web/assets/browser-chrome.svg +++ b/web/assets/browser-chrome.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/web/assets/browser-edge.svg b/web/assets/browser-edge.svg index 4f95f8de9..4fd54d839 100644 --- a/web/assets/browser-edge.svg +++ b/web/assets/browser-edge.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/web/assets/browser-firefox.svg b/web/assets/browser-firefox.svg index 865424c0a..bd7439d83 100644 --- a/web/assets/browser-firefox.svg +++ b/web/assets/browser-firefox.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/web/assets/flags/bd.svg b/web/assets/flags/bd.svg index 16b794deb..8bafc11ef 100644 --- a/web/assets/flags/bd.svg +++ b/web/assets/flags/bd.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/web/assets/flags/br.svg b/web/assets/flags/br.svg index 719a763ca..e3eff4b36 100644 --- a/web/assets/flags/br.svg +++ b/web/assets/flags/br.svg @@ -1,45 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/cn.svg b/web/assets/flags/cn.svg index 10d3489a0..d4cab2ef4 100644 --- a/web/assets/flags/cn.svg +++ b/web/assets/flags/cn.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/de.svg b/web/assets/flags/de.svg index 71aa2d2c3..16a0041be 100644 --- a/web/assets/flags/de.svg +++ b/web/assets/flags/de.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/web/assets/flags/fr.svg b/web/assets/flags/fr.svg index 4110e59e4..8f923f993 100644 --- a/web/assets/flags/fr.svg +++ b/web/assets/flags/fr.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/web/assets/flags/id.svg b/web/assets/flags/id.svg index 3b7c8fcfd..2d908da13 100644 --- a/web/assets/flags/id.svg +++ b/web/assets/flags/id.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/web/assets/flags/il.svg b/web/assets/flags/il.svg index f43be7e8e..bd779de9d 100644 --- a/web/assets/flags/il.svg +++ b/web/assets/flags/il.svg @@ -1,14 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/in.svg b/web/assets/flags/in.svg index bc47d7491..8c9991e8b 100644 --- a/web/assets/flags/in.svg +++ b/web/assets/flags/in.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/ir.svg b/web/assets/flags/ir.svg index 8c6d51621..606c4d39b 100644 --- a/web/assets/flags/ir.svg +++ b/web/assets/flags/ir.svg @@ -1,219 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/jp.svg b/web/assets/flags/jp.svg index cc1c181ce..b84407ff3 100644 --- a/web/assets/flags/jp.svg +++ b/web/assets/flags/jp.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/kr.svg b/web/assets/flags/kr.svg index 6947eab2b..6146fef35 100644 --- a/web/assets/flags/kr.svg +++ b/web/assets/flags/kr.svg @@ -1,24 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/my.svg b/web/assets/flags/my.svg index 115f864d8..c593a5516 100644 --- a/web/assets/flags/my.svg +++ b/web/assets/flags/my.svg @@ -1,26 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/nl.svg b/web/assets/flags/nl.svg index e90f5b035..cd4ee2704 100644 --- a/web/assets/flags/nl.svg +++ b/web/assets/flags/nl.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/web/assets/flags/ph.svg b/web/assets/flags/ph.svg index b910e24cd..1dcce8bf6 100644 --- a/web/assets/flags/ph.svg +++ b/web/assets/flags/ph.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/ru.svg b/web/assets/flags/ru.svg index cf243011a..8ff50c9ba 100644 --- a/web/assets/flags/ru.svg +++ b/web/assets/flags/ru.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/web/assets/flags/sa.svg b/web/assets/flags/sa.svg index 596cf48bb..2f49621a0 100644 --- a/web/assets/flags/sa.svg +++ b/web/assets/flags/sa.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/th.svg b/web/assets/flags/th.svg index 1e93a61e9..1a441d1f7 100644 --- a/web/assets/flags/th.svg +++ b/web/assets/flags/th.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/tr.svg b/web/assets/flags/tr.svg index b96da21f0..ecd5fe5f6 100644 --- a/web/assets/flags/tr.svg +++ b/web/assets/flags/tr.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/ua.svg b/web/assets/flags/ua.svg index a339eb1b9..51eacf098 100644 --- a/web/assets/flags/ua.svg +++ b/web/assets/flags/ua.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/us.svg b/web/assets/flags/us.svg index 9cfd0c927..72895617b 100644 --- a/web/assets/flags/us.svg +++ b/web/assets/flags/us.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/flags/vn.svg b/web/assets/flags/vn.svg index 7e4bac8f4..69d6aa1f5 100644 --- a/web/assets/flags/vn.svg +++ b/web/assets/flags/vn.svg @@ -1,11 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/web/assets/marktechpost.svg b/web/assets/marktechpost.svg index 4773cbdd9..cce4dd8d9 100644 --- a/web/assets/marktechpost.svg +++ b/web/assets/marktechpost.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/web/assets/providers/anthropic.svg b/web/assets/providers/anthropic.svg index 5b81844cb..9aa4e4189 100644 --- a/web/assets/providers/anthropic.svg +++ b/web/assets/providers/anthropic.svg @@ -1 +1 @@ -Anthropic \ No newline at end of file +Anthropic \ No newline at end of file diff --git a/web/assets/providers/deepseek.svg b/web/assets/providers/deepseek.svg index 3fc230240..580f3edc8 100644 --- a/web/assets/providers/deepseek.svg +++ b/web/assets/providers/deepseek.svg @@ -1 +1 @@ -DeepSeek \ No newline at end of file +DeepSeek \ No newline at end of file diff --git a/web/assets/providers/gemini.svg b/web/assets/providers/gemini.svg index f1cf35757..33a088c19 100644 --- a/web/assets/providers/gemini.svg +++ b/web/assets/providers/gemini.svg @@ -1 +1 @@ -Gemini \ No newline at end of file +Gemini \ No newline at end of file diff --git a/web/assets/providers/llamacpp.svg b/web/assets/providers/llamacpp.svg index 613e8da25..f6bbe19ec 100644 --- a/web/assets/providers/llamacpp.svg +++ b/web/assets/providers/llamacpp.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/web/assets/providers/lmstudio.svg b/web/assets/providers/lmstudio.svg index ea0816b63..ac29870af 100644 --- a/web/assets/providers/lmstudio.svg +++ b/web/assets/providers/lmstudio.svg @@ -1 +1 @@ -LM Studio \ No newline at end of file +LM Studio \ No newline at end of file diff --git a/web/assets/providers/mistral.svg b/web/assets/providers/mistral.svg index 8e03e244b..8d4f2bb23 100644 --- a/web/assets/providers/mistral.svg +++ b/web/assets/providers/mistral.svg @@ -1 +1 @@ -Mistral \ No newline at end of file +Mistral \ No newline at end of file diff --git a/web/assets/providers/ollama.svg b/web/assets/providers/ollama.svg index cc887e3dc..aebc3abf4 100644 --- a/web/assets/providers/ollama.svg +++ b/web/assets/providers/ollama.svg @@ -1 +1 @@ -Ollama \ No newline at end of file +Ollama \ No newline at end of file diff --git a/web/assets/providers/openai.svg b/web/assets/providers/openai.svg index 50d94d6c1..61342bd19 100644 --- a/web/assets/providers/openai.svg +++ b/web/assets/providers/openai.svg @@ -1 +1 @@ -OpenAI \ No newline at end of file +OpenAI \ No newline at end of file diff --git a/web/assets/providers/openrouter.svg b/web/assets/providers/openrouter.svg index e6cca2a86..e51b58d3e 100644 --- a/web/assets/providers/openrouter.svg +++ b/web/assets/providers/openrouter.svg @@ -1 +1 @@ -OpenRouter \ No newline at end of file +OpenRouter \ No newline at end of file diff --git a/web/assets/providers/vllm.svg b/web/assets/providers/vllm.svg index 54acc3de2..ba278c758 100644 --- a/web/assets/providers/vllm.svg +++ b/web/assets/providers/vllm.svg @@ -1 +1 @@ -vLLM \ No newline at end of file +vLLM \ No newline at end of file diff --git a/web/assets/providers/xai.svg b/web/assets/providers/xai.svg index 536e71390..d12e6ba00 100644 --- a/web/assets/providers/xai.svg +++ b/web/assets/providers/xai.svg @@ -1 +1 @@ -Grok \ No newline at end of file +Grok \ No newline at end of file From 7d1e474c0ca30e8197b7c9df2bf03b5df4d57927 Mon Sep 17 00:00:00 2001 From: Gaurav Singh <87198156+gaur-avvv@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:23:18 +0530 Subject: [PATCH 2/6] fix(content): prove non-sends in recipient probe, reliable text-click locator, content-script self-heal, candidate cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recipient-guard probe no longer treats "inconclusive" as blockable: added proven-non-send classifiers (composer utilities like "Save & close" / "Minimize" / formatting / attach, far-from-composer controls, no-composer pages) so ordinary clicks on LinkedIn/Gmail dispatch instead of looping. - Read typed-but-not-chipified recipients from To/Cc/Bcc input values, plus generic recipient fields (type=email, autocomplete=email, multilingual To/recipient labels) and address chips (mailto:, [data-email], [email]) — confirm-then-send now works on any site, not just Gmail. - click({text}) resolves reliably: collapse matches that share an actionable ancestor (label-wrapped "Send" buttons), short contains needles need a word boundary (fixes click({text:"X"}) matching 13 controls), ambiguity is narrowed by the hit-target ("receives events") rule and auto-resolved when exactly one candidate survives. - _clickTextCandidates is cached per scope and invalidated by one attribute-filtered MutationObserver, removing a full-DOM walk from every text click, preflight, and retry. - Self-healing after extension reload: a generation counter replaces the boolean double-injection guard so the newest install always takes over, and the message dispatcher always answers (sync throws and empty results become explicit errors instead of a silently closed channel). --- src/chrome/src/content/content.js | 518 ++++++++++++++++++++++++++++-- 1 file changed, 496 insertions(+), 22 deletions(-) diff --git a/src/chrome/src/content/content.js b/src/chrome/src/content/content.js index 1a2b3d951..2f71e4248 100644 --- a/src/chrome/src/content/content.js +++ b/src/chrome/src/content/content.js @@ -4,8 +4,25 @@ */ (() => { - // Prevent double-injection - if (window.__webbrain_injected) return; + // Prevent double-injection within one extension context — but TAKE OVER + // from an orphaned installation. When the extension is reloaded, updated, + // or its folder moves, the previous content script stays in the page with + // a dead runtime context: it still receives messages but can never answer + // them, and because `__webbrain_injected` is already set, re-injecting the + // current build used to be a no-op. That made every page tool fail with + // "received no answer" until the tab was manually reloaded. + // + // A monotonic generation counter fixes this: each install bumps + // `__webbrain_generation`, and only the listener belonging to the newest + // install answers. Older (possibly orphaned) listeners stay registered but + // go inert, and a fresh install always takes over. + window.__webbrain_generation = (Number(window.__webbrain_generation) || 0) + 1; + const WEBBRAIN_GENERATION = window.__webbrain_generation; + // No early return: a fresh install ALWAYS takes over. Re-running the body + // is safe (everything lives inside this IIFE, so no const redeclaration), + // and the generation check in the message handler below makes every + // previous copy inert, so exactly one listener ever answers. Orphaned + // duplicates only linger as inert JS until the tab is reloaded. window.__webbrain_injected = true; const RECORDING_DOUBLE_ESCAPE_MS = 1400; @@ -247,7 +264,8 @@ const descriptor = window.__wbSiteInteractions?.describe?.(el); if (descriptor?.name) return descriptor.name; } catch {} - return (el?.innerText || el?.value || el?.placeholder || el?.title || el?.ariaLabel || '').trim(); + const aria = el?.getAttribute?.('aria-label') || el?.ariaLabel || ''; + return (el?.innerText || el?.value || el?.placeholder || el?.title || aria || '').trim(); } function _isSiteInteractive(el) { @@ -1367,11 +1385,46 @@ // Shared by click dispatch and its recipient-safety preflight. In // particular, navigation links must participate in text-match ambiguity. + // + // This walk is the most expensive part of a text click: it scans the page + // (including every shadow host) and reads innerText per match. clickElement, + // the recipient preflight, and every retry all call it. Cache the result and + // reuse it until the DOM actually changes. + const _domRevision = { value: 0, observer: null }; + function _ensureDomRevisionObserver() { + if (_domRevision.observer || typeof MutationObserver === 'undefined') return; + try { + _domRevision.observer = new MutationObserver(() => { _domRevision.value += 1; }); + _domRevision.observer.observe(document, { + childList: true, + subtree: true, + attributes: true, + // Only the attributes that can change a text-click match; a broad + // filter keeps the callback cheap on busy SPAs. + attributeFilter: [ + 'aria-label', 'aria-labelledby', 'title', 'placeholder', 'value', 'disabled', + 'aria-expanded', 'aria-selected', 'aria-checked', 'aria-hidden', 'hidden', + 'class', 'style', 'role', 'tabindex', + ], + }); + } catch { + _domRevision.observer = null; + } + } + + const _textCandidateCache = new WeakMap(); function _clickTextCandidates(scope) { + _ensureDomRevisionObserver(); + const revision = _domRevision.value; + const cacheScope = scope && typeof scope === 'object' ? scope : document; + const cached = _textCandidateCache.get(cacheScope); + if (cached && cached.revision === revision) return cached.list.slice(); const selectors = [ 'a', 'button', '[role="button"]', '[role="link"]', '[role="tab"]', '[role="menuitem"]', + '[role="menuitemcheckbox"]', '[role="combobox"]', '[role="searchbox"]', '[role="textbox"]', 'input:not([type="hidden"])', 'textarea', 'select', 'input[type="button"]', 'input[type="submit"]', 'summary', 'label', '[onclick]', '[data-action]', + '[aria-label]', '[tabindex]', '[data-tooltip]', ..._siteInteractiveSelectors(), ].join(', '); // LinkedIn can render the feed itself inside an open shadow root. Keep @@ -1385,11 +1438,24 @@ if (host.shadowRoot) visit(host.shadowRoot); } }; - visit(scope); - return candidates - .filter(e => _hasVisibleBox(e)) - .map(e => ({ e, txt: _siteInteractionText(e).toLowerCase() })) - .filter(candidate => candidate.txt); + visit(cacheScope); + const out = []; + const seen = new Set(); + for (const e of candidates) { + if (!_hasVisibleBox(e)) continue; + const text = _siteInteractionText(e).toLowerCase(); + if (text && !seen.has(`${text}\u0000${out.length}`)) out.push({ e, txt: text }); + // A control's aria-label / title / placeholder often names it far better + // than its visible glyphs — icon-only buttons and combobox-style fields + // ("Search for contacts") are unreachable by visible text alone. Offer + // those as extra needles so a text click resolves in one round trip. + for (const attr of ['aria-label', 'title', 'placeholder', 'data-tooltip']) { + const value = String(e.getAttribute?.(attr) || '').trim().toLowerCase(); + if (value && value !== text) out.push({ e, txt: value }); + } + } + _textCandidateCache.set(cacheScope, { revision, list: out }); + return out.slice(); } function _shadowAwareElementFromPoint(x, y) { @@ -1463,7 +1529,18 @@ function tryMode(mode) { if (mode === 'exact') return normalized.filter(x => x.txt === needle); if (mode === 'prefix') return normalized.filter(x => x.txt.startsWith(needle)); - if (mode === 'contains') return normalized.filter(x => x.txt.includes(needle)); + if (mode === 'contains') { + // A 1-2 character needle in contains mode matches half the page: + // click({text:"X"}) matched 13 controls and burned an entire turn on + // an ambiguity report. Short needles must hit a word boundary so they + // can only resolve to something that is actually that token. + if (needle.length >= 3) return normalized.filter(x => x.txt.includes(needle)); + const padded = ` ${needle} `; + return normalized.filter(x => x.txt === needle + || x.txt.startsWith(`${needle} `) + || x.txt.endsWith(` ${needle}`) + || x.txt.includes(padded)); + } return []; } @@ -1543,12 +1620,66 @@ return { success: false, dispatched: false, error: `No clickable element found for text "${params.text}" (also tried scrolling down)${_noteModal}` }; } } + // Collapse text matches that resolve to the SAME actionable element + // before treating the text as ambiguous. An icon+label button wraps its + // text in nested divs/spans, so an exact text match legitimately matches + // the control AND its inner text node — two matches, one button. Gmail's + // "Send" is exactly this shape, which made an unambiguous click look + // ambiguous and sent the model off to do coordinate arithmetic. + // Playwright-style locators resolve to the element that actually + // receives the click; doing that here fixes "Send", "Post", "Connect", + // and every other label-wrapped control on the web. + if (!el && matches.length > 1) { + const collapsedByAncestor = new Map(); + for (const m of matches) { + const actionable = _resolveInteractiveAncestor(m.e) || m.e; + if (!collapsedByAncestor.has(actionable)) collapsedByAncestor.set(actionable, m); + } + if (collapsedByAncestor.size < matches.length) { + matches = [...collapsedByAncestor.values()]; + } + if (matches.length === 1) { + el = _resolveInteractiveAncestor(matches[0].e) || matches[0].e; + textResolvedExact = (usedMode === 'exact'); + } + } if (!el && matches.length > 1) { // Prefer interactive elements over passive children (label, span, etc.) const interactiveMatches = matches.filter(m => _isInteractive(m.e)); if (interactiveMatches.length === 1) { matches = interactiveMatches; } else { + // Playwright-style narrowing: a click must land on the element that + // actually receives the event. Prefer candidates that are their own + // hit target — visible, enabled, not covered by an overlay. This + // usually collapses a reported ambiguity to the one control a real + // click would hit, and often to a single element, which then resolves + // here instead of spending an entire LLM turn on candidate + // coordinates. + const hitTargetPool = (interactiveMatches.length > 1 ? interactiveMatches : matches).filter(m => { + try { + const r = m.e.getBoundingClientRect(); + if (r.width < 1 || r.height < 1) return false; + if (m.e.disabled === true || m.e.getAttribute?.('aria-disabled') === 'true') return false; + const hit = _shadowAwareElementFromPoint( + Math.round(r.left + r.width / 2), + Math.round(r.top + r.height / 2), + ); + return !!hit && (hit === m.e || m.e.contains?.(hit) || hit.contains?.(m.e)); + } catch { + return false; + } + }); + if (hitTargetPool.length === 1) { + el = _resolveInteractiveAncestor(hitTargetPool[0].e) || hitTargetPool[0].e; + textResolvedExact = (usedMode === 'exact'); + matches = hitTargetPool; + } else if (hitTargetPool.length > 1 && hitTargetPool.length < matches.length) { + // Still ambiguous, but report only the controls a real click could + // hit — fewer, better candidates for the one turn it costs. + matches = hitTargetPool; + } + if (matches.length > 1) { // Build rich candidates: position (rect), tag, role, surrounding // context (closest landmark/dialog/button text), and a suggested // disambiguator. When the same text appears twice (e.g. "Cancel" @@ -1598,6 +1729,7 @@ error: `Ambiguous text match for "${params.text}" (mode=${usedMode}, matches=${matches.length})${_scopeNote}. ${candidates.length} candidates returned with cx/cy (precomputed click center, in CSS pixels) and ancestor context. Pick one and call click({x: candidate.cx, y: candidate.cy, coordinate_space: "css"}) — no arithmetic needed. Use the ancestor field to disambiguate (e.g. an alertdialog's Cancel vs a form's Cancel sit in different containers). Do NOT retry click({text: "${params.text}"}) — it will fail the same way.`, candidates, }; + } } } if (!el) { @@ -4970,6 +5102,77 @@ } } + // A control whose label can commit a message in ONE activation, with no + // composer rendered ("Send", "Send without a note", "Send invitation"). + // Everything else on a composer-less page is provably not a message send. + const _MESSAGE_COMMIT_LABEL_RE = new RegExp( + '(?:^|[^\\p{L}])(?:send|enviar|envoyer|invia|senden|verzenden|gönder|отправить|送信|发送|發送|보내|إرسال|ارسال)(?:[^\\p{L}]|$)', + 'iu', + ); + // Containers the messaging apps themselves use for their messaging surface. + // Anything inside them stays on the recipient-verification path even when no + // composer is currently rendered. + const _MESSAGING_SURFACE_SELECTOR = [ + '[class*="msg-"]', + '[class*="messaging"]', + '[id*="messaging"]', + '[data-messaging]', + '[data-test-messaging]', + '[role="log"]', + ].join(','); + + function _hasMessageCommitName(value) { + const text = String(value ?? '').replace(/\s+/g, ' ').trim(); + return !!text && _MESSAGE_COMMIT_LABEL_RE.test(text); + } + + function _inMessagingSurface(el) { + try { + return !!el?.closest?.(_MESSAGING_SURFACE_SELECTOR); + } catch { + return false; + } + } + + // Composer utilities sit at the composer's edge, so the geometry rule would + // otherwise treat them as potential sends: Gmail's own toolbar exposes + // "Save & close", "Minimize", "Exit full screen (Shift for Pop-out)", plus + // attach/formatting/discard controls. None of them can commit a message. + // Without this list a click on "Save & close" was reported as a blocked + // message send and the task could never finish. + const _COMPOSER_UTILITY_LABEL_RE = new RegExp( + '^(?:save\\s*(?:&|and)?\\s*(?:close|draft|draft\\s*&\\s*close)|save\\s*&\\s*close' + + '|minimi[sz]e|exit\\s+full\\s+screen|full\\s+screen|pop-?out|expand|collapse' + + '|discard(?:\\s+draft)?|delete\\s+draft|print|check\\s+spelling|plain\\s+text(?:\\s+mode)?' + + '|attach(?:\\s+(?:files?|photos?|images?|documents?))?|insert\\s+(?:link|photo|image|file|drive|emoji|signature|contact|table|drawing|note)' + + '|emoji|emoticon|bold|italic|underline|strikethrough|align|numbered\\s+list|bulleted\\s+list' + + '|indent|outdent|undo|redo|more\\s+options|formatting(?:\\s+options)?|text\\s+formatting' + + '|remove\\s+formatting|font|text\\s+color|highlight|edit\\s+subject|confidential\\s+mode)', + 'i', + ); + + function _isComposerUtilityControl(el) { + if (!el) return false; + try { + const label = String( + el.getAttribute?.('aria-label') + || el.getAttribute?.('title') + || el.getAttribute?.('data-tooltip') + || el.value + || el.innerText + || el.textContent + || '', + ).replace(/\s+/g, ' ').trim(); + if (label && _COMPOSER_UTILITY_LABEL_RE.test(label)) return true; + // Formatting toolbars and pressed toggles are structural non-sends. + if (el.closest?.('[role="toolbar"],[aria-label*="formatting" i],[aria-label*="format" i]')) return true; + if (el.hasAttribute?.('aria-pressed')) return true; + return false; + } catch { + return false; + } + } + // Read-only pre-dispatch probe for adapters that require a verified active // conversation before a message can be sent. It deliberately ignores input // values and ordinary page text: a searched recipient name is not proof that @@ -5170,7 +5373,15 @@ } else if (tool === 'click') { // Match dispatch precedence, modal scope, candidate text and match // mode. A supplied selector must not approve a different text click. - if (typeof args.text === 'string' && args.text) { + // Models routinely pass a ref_id to `click` instead of `click_ax`; the + // probe must resolve that the same way the dispatch will, otherwise a + // perfectly good click was reported as "could not conclusively resolve + // the target control". + if (typeof args.ref_id === 'string' && args.ref_id + && typeof window.__wb_ax_lookup === 'function') { + target = window.__wb_ax_lookup(args.ref_id); + } + if (!target && typeof args.text === 'string' && args.text) { const needle = args.text.toLowerCase(); const scope = _findTopmostModal() || document; const candidates = _clickTextCandidates(scope); @@ -5188,11 +5399,11 @@ if (matches.length === 1) target = _resolveInteractiveAncestor(matches[0].e); if (matches.length) break; } - } else if (typeof args.selector === 'string' && args.selector) { + } else if (!target && typeof args.selector === 'string' && args.selector) { target = safeIndexedQuerySelector(args.selector, args.matchIndex).element; - } else if (Number.isInteger(args.index) && args.index >= 0) { + } else if (!target && Number.isInteger(args.index) && args.index >= 0) { target = queryInteractiveForToolIndex()[args.index] || null; - } else if (Number.isFinite(args.x) && Number.isFinite(args.y)) { + } else if (!target && Number.isFinite(args.x) && Number.isFinite(args.y)) { target = _shadowAwareElementFromPoint(args.x, args.y); } targetResolved = !!target; @@ -5479,9 +5690,26 @@ return { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; } if (active !== layoutComposer) { - return verifiedNavigationEditable(active) - ? { success: true, messageSend: false, conclusive: true, identityCandidates: [] } - : { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; + if (verifiedNavigationEditable(active)) { + return { success: true, messageSend: false, conclusive: true, identityCandidates: [] }; + } + // Editables that share the compose surface with the message body — + // Gmail's To / Subject inputs, chip and autocomplete fields — cannot + // send a message on their own, but Enter is how users commit an + // autocomplete pick and Tab moves between fields. Blocking those + // presses turned normal compose navigation into a dead end. Only a + // key inside the composer itself can dispatch a message, and that + // path below stays fully guarded. + try { + const composeScopeOf = (el) => el?.closest?.( + 'form, [role="dialog"], [role="form"], [aria-label*="compose" i], [class*="compose" i]', + ); + const activeScope = composeScopeOf(active); + if (activeScope && activeScope === composeScopeOf(layoutComposer)) { + return { success: true, messageSend: false, conclusive: true, identityCandidates: [] }; + } + } catch { /* fall through to the conservative result */ } + return { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; } composer = layoutComposer; dispatchTarget = active; @@ -5534,6 +5762,23 @@ identityCandidates: [], }; } + // Composer utilities (Gmail's "Save & close" / "Minimize" / "Exit full + // screen / Pop-out", attach, formatting, discard, undo) live at the + // composer's edge, so the geometry rule below would otherwise classify + // them as potential sends and block them — which is how a click on + // "Save & close" came back as a blocked message send and the task + // could never finish. None of them can commit a message, so they are + // proven non-sends regardless of composer presence or geometry. + if (_isComposerUtilityControl(control)) { + return { + success: true, + messageSend: false, + conclusive: true, + composerUtility: true, + reasonCode: 'non_messaging_target', + identityCandidates: [], + }; + } composer = layoutComposer; if (!composer) { const actionLabel = compact( @@ -5547,11 +5792,36 @@ ); const composerSetup = params.adapterName === 'gmail' && /^(?:reply|reply all|forward)$/i.test(actionLabel); + // The guard exists to keep a MESSAGE from reaching the wrong + // recipient. When the page exposes no composer at all, an ordinary + // control (Connect, Follow, Like, a feed row, Inbox nav, a search + // box) cannot send a message in this activation, so it is proven + // not to be a send and must not be blocked. Only controls that can + // still commit without a composer — a send/commit-labelled control, + // a messaging app's Reply/Reply all/Forward opener, or anything + // inside the app's messaging surface — stay fail-closed. + const provenNonMessage = !composerSetup + && !_hasMessageCommitName(actionLabel) + && !_inMessagingSurface(control); + if (provenNonMessage) { + return { + success: true, + messageSend: false, + conclusive: true, + composerAvailable: false, + nonMessagingTarget: true, + reasonCode: 'non_messaging_target', + identityCandidates: [], + }; + } return { success: true, messageSend: null, conclusive: false, composerAvailable: false, + reasonCode: _hasMessageCommitName(actionLabel) + ? 'message_commit_without_composer' + : 'no_composer_surface', ...(composerSetup ? { composerSetup: true } : {}), identityCandidates: [], }; @@ -5568,17 +5838,49 @@ identityCandidates: [], }; } - dispatchTarget = target; + dispatchTarget = target; const composerRect = composer.getBoundingClientRect(); const controlRect = control.getBoundingClientRect(); + // Geometry: controls visually next to the composer are the message-send + // affordance. Sending from a control that is far from the composer is + // almost always wrong on well-behaved chat surfaces, and blocking it there + // gives the model a precise, retryable failure instead of silently sending + // to the wrong place. Two carpet tolerances are deliberately loose here so + // that non-messaging apps (email, forms, social buttons) are not treated + // as messenger controls purely by proximity — they will still end up going + // through the generic 'inconclusive' path above, which the agent can + // reclassify from the surrounding text on the next read. + const sameForm = !!composer.closest?.('form') && composer.closest('form') === control.closest?.('form'); const horizontalGap = Math.max(0, composerRect.left - controlRect.right, controlRect.left - composerRect.right); const verticalGap = Math.max(0, composerRect.top - controlRect.bottom, controlRect.top - composerRect.bottom); - const sameForm = !!composer.closest?.('form') && composer.closest('form') === control.closest?.('form'); - // Framework chat controls are often clickable divs rather than native - // buttons, and a nearby control can send an attachment even when the - // text composer is empty. Geometry must therefore win over tag shape. messageSend = sameForm || (horizontalGap <= 240 && verticalGap <= 120); if (!messageSend) { + // The control is geometrically far from the composer and not in the + // same form, so clicking it cannot commit the message. Geometry alone + // is enough evidence — but only after also ruling out a send-labelled + // control and anything inside the app's messaging surface, both of + // which can commit without sitting near the composer. That second + // check is what makes far-away controls self-healing instead of + // being blocked as "inconclusive" and retried in a loop. + const farLabel = compact( + control.getAttribute?.('aria-label') + || control.getAttribute?.('title') + || control.getAttribute?.('data-tooltip') + || control.value + || control.innerText + || control.textContent, + 120, + ); + if (!_hasMessageCommitName(farLabel) && !_inMessagingSurface(control)) { + return { + success: true, + messageSend: false, + conclusive: true, + nonMessagingTarget: true, + reasonCode: 'non_messaging_target', + identityCandidates: [], + }; + } return { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; } } @@ -5744,6 +6046,55 @@ })(); gmailComposeRoot = composeRoot; const recipients = collectGmailRecipients(composeRoot); + // A recipient that was typed but not yet turned into a chip lives only + // in the To/Cc/Bcc input VALUE — there is no [email] attribute to read. + // Without this pass a compose window whose address was just typed + // reported zero observed recipients, so the guard could neither name + // the address nor bind a later user confirmation to what is on screen. + const collectRecipientInputValues = (root) => { + if (!root?.querySelectorAll) return; + let inputs = []; + try { + inputs = Array.from(root.querySelectorAll( + 'input, textarea, [contenteditable="true"], [contenteditable=""]', + )).slice(0, 40); + } catch { + return; + } + for (const input of inputs) { + if (!visible(input)) continue; + if (input === composer || composer?.contains?.(input)) continue; + const role = roleFromAttribute('name', input.getAttribute?.('name')) + || roleFromAttribute('aria-label', input.getAttribute?.('aria-label')) + || ''; + if (!role) continue; + const raw = String( + 'value' in input ? (input.value || '') : (input.innerText || input.textContent || ''), + ); + if (!raw || raw.length > 2000) continue; + for (const piece of raw.split(/[,;\n]+/)) { + const candidate = compact(piece, 240); + if (!candidate) continue; + const match = candidate.match(/[^\s<>@,;]+@[^\s<>@,;.]+\.[^\s<>@,;]+/); + if (!match) continue; + const email = match[0]; + const key = normalizedIdentity(email); + if (!key) continue; + const recipientKey = `${role}:${key}`; + const prior = recipients.get(recipientKey) || { identity: email, role, aliases: new Map() }; + prior.aliases.set(key, email); + const display = compact(candidate.replace(/<[^>]*>/g, ''), 240); + if (display && display !== email) { + const normalizedDisplay = normalizedIdentity(display); + if (normalizedDisplay && !prior.aliases.has(normalizedDisplay)) { + prior.aliases.set(normalizedDisplay, display); + } + } + recipients.set(recipientKey, prior); + } + } + }; + collectRecipientInputValues(composeRoot); for (const [recipientKey, recipient] of recipients) { const emailKey = recipientKey.slice(recipientKey.indexOf(':') + 1); const identity = recipient.aliases.get(emailKey) || [...recipient.aliases.values()][0] || recipient.identity || ''; @@ -5861,6 +6212,100 @@ } } + // ── Generic recipient discovery (every site) ──────────────────────── + // The Gmail branch above reads Google's own DOM. Every other webmail, + // CRM, ticketing tool, and messaging app needs the same answer: who would + // this send actually go to? Read it structurally — a recipient-labelled + // field, or an address-bearing chip/link near the composer — instead of + // depending on one vendor's markup. This is what makes confirm-then-send + // work globally rather than only on Gmail. + if (observedRecipientCandidates.length === 0) { + const RECIPIENT_FIELD_RE = new RegExp( + '(?:^|[^\\p{L}])(?:to|to\\s+recipients?|recipients?|send\\s+to|email|e-?mail(?:\\s+address)?' + + '|destinatario|destinataria|destinataire|para|aan|kime|do|til|komu|\\u0644\\u0625\\u0649' + + '|\\u6536\\u4ef6\\u4eba|\\u5b9b\\u5148|\\ubc1b\\ub294\\s*\\uc0ac\\ub78c)(?:[^\\p{L}]|$)', + 'iu', + ); + const fieldLooksLikeRecipient = (el) => { + try { + if (/^email$/i.test(String(el.getAttribute?.('type') || ''))) return true; + if (/^email$/i.test(String(el.getAttribute?.('autocomplete') || ''))) return true; + const haystack = [ + el.getAttribute?.('name'), + el.getAttribute?.('aria-label'), + el.getAttribute?.('placeholder'), + el.getAttribute?.('id'), + el.getAttribute?.('data-testid'), + el.getAttribute?.('autocomplete'), + el.labels ? Array.from(el.labels).map(label => label.innerText).join(' ') : '', + ].filter(Boolean).join(' '); + return !!haystack && RECIPIENT_FIELD_RE.test(haystack); + } catch { + return false; + } + }; + const pushRecipient = (identity, extraAliases = []) => { + const raw = compact(identity, 240); + const normalized = raw ? normalizedIdentity(raw) : ''; + if (!normalized) return; + const existing = observedRecipientCandidates.find( + item => normalizedIdentity(item.identity) === normalized, + ); + const aliases = Array.from(new Set([ + raw, + ...extraAliases, + ...(existing?.aliases || []), + ].filter(Boolean))); + const entry = { identity: raw, role: 'to', aliases }; + if (existing) Object.assign(existing, entry); + else observedRecipientCandidates.push(entry); + if (!strongRecipients.some(item => normalizedIdentity(item.identity) === normalized)) { + strongRecipients.push({ identity: raw, role: 'to' }); + } + }; + const extractAddresses = (rawValue) => { + const raw = String(rawValue || ''); + if (!raw || raw.length > 2000) return; + for (const piece of raw.split(/[,;\n]+/)) { + const candidate = compact(piece, 240); + if (!candidate) continue; + const match = candidate.match(/[^\s<>@,;]+@[^\s<>@,;.]+\.[^\s<>@,;]+/); + if (!match) continue; + const display = compact(candidate.replace(/<[^>]*>/g, ''), 240); + pushRecipient(match[0], display && display !== match[0] ? [display] : []); + } + }; + try { + for (const el of Array.from(document.querySelectorAll( + 'input, textarea, [contenteditable="true"], [contenteditable=""]', + )).slice(0, 60)) { + if (!visible(el) || el === composer || composer?.contains?.(el)) continue; + if (!fieldLooksLikeRecipient(el)) continue; + extractAddresses('value' in el ? el.value : (el.innerText || el.textContent || '')); + } + } catch { /* structural read is best-effort */ } + try { + const chipScope = composer.closest?.('[role="dialog"],form,[role="main"]') || document; + for (const el of Array.from(chipScope.querySelectorAll( + '[email],[data-email],[data-hovercard-id],a[href^="mailto:" i],input[type="email"]', + )).slice(0, 40)) { + if (!visible(el) || el === composer || composer?.contains?.(el)) continue; + const mailto = String(el.getAttribute?.('href') || '') + .replace(/^mailto:/i, '').split('?')[0]; + const address = [ + el.getAttribute?.('email'), + el.getAttribute?.('data-email'), + el.getAttribute?.('data-hovercard-id'), + mailto, + 'value' in el ? el.value : '', + ].map(value => compact(value, 240)).find(value => String(value || '').includes('@')) || ''; + if (!address) continue; + const display = compact(el.innerText || el.textContent, 240); + pushRecipient(address, display && display !== address ? [display] : []); + } + } catch { /* chip read is best-effort */ } + } + const composerText = (() => { try { if ('value' in composer) return String(composer.value || ''); @@ -5970,6 +6415,12 @@ // --- Message handler --- chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.target !== 'content') return; + // Only the newest installed copy answers. An orphaned copy from a + // previous extension load still has a live DOM but a dead runtime + // context — it receives this message and can never respond, which used + // to shadow the healthy listener and turn every page tool into + // "received no answer". Yielding here lets the current install reply. + if (WEBBRAIN_GENERATION !== (Number(window.__webbrain_generation) || 0)) return; const actionDeadlineAt = Number(msg.actionDeadlineAt) || 0; const actionDeadlineExpired = () => actionDeadlineAt > 0 && Date.now() >= actionDeadlineAt; if (actionDeadlineExpired()) { @@ -7667,7 +8118,19 @@ return value; }; - const result = handler(); + let result; + try { + result = handler(); + } catch (err) { + // A synchronous throw used to escape the listener without calling + // sendResponse, so the caller saw an undefined response and reported + // " returned no result." with no diagnosis. Always answer. + sendResponse(withLiveDocumentScope({ + success: false, + error: `${msg.action} failed: ${err?.message || String(err)}`, + })); + return; + } if (result instanceof Promise) { // Always settle sendResponse — a rejecting handler (e.g. a throwing // DOM API) must not leave the caller's await hanging forever. @@ -7680,6 +8143,17 @@ ); return true; // async } + if (result === undefined || result === null) { + // A handler that answers with nothing is a bug, not an empty result: + // surface it so the caller can distinguish a real failure from a + // silently closed response channel. + sendResponse(withLiveDocumentScope({ + success: false, + noResult: true, + error: `${msg.action} returned no result (handler produced no payload).`, + })); + return; + } sendResponse(withLiveDocumentScope(result)); }); From d4bd55a6dcf6081f532cdefe18d1800b3a4642ac Mon Sep 17 00:00:00 2001 From: Gaurav Singh <87198156+gaur-avvv@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:25:13 +0530 Subject: [PATCH 3/6] fix(agent): fail-open recipient guard, user-granted send authorization, silent-reply recovery, screenshot budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recipient guard no longer blocks unclassifiable actions — only a confident send classification reaches verification; everything else dispatches. - A bound recipient clarification authorizes the send even when the planner's target differs: guard.messaging is re-pointed at the observed recipients, approvals are remembered per-recipient for the task (guard.approvedRecipients), and a "send all remaining" answer covers the rest of a multi-email task (guard.messageRecipientApprovedAll). - The mismatch block names the page-observed recipient and prescribes the exact clarify call (address as an option + "Send all remaining" + Cancel) instead of looping. - Generic surfaces with no recipient identity (contact/feedback forms) fail open to the submit-confirmation gate instead of looping. - Silent content-action replies (undefined) force-inject the current content scripts and resend once, with a precise staleContentScript error if that still fails. - _shouldAutoScreenshot is repetition-aware: first action of a burst captures, then only every 4th; a different action resets; honors domEvidenceProven and the existing maxScreenshotsPerTurn budget. An 8-click burst now costs ~2 screenshots + 2 vision calls instead of 8 + 8. --- src/chrome/src/agent/agent.js | 214 +++++++++++++++++++++++++++++++--- 1 file changed, 200 insertions(+), 14 deletions(-) diff --git a/src/chrome/src/agent/agent.js b/src/chrome/src/agent/agent.js index cbc65e4f0..064149735 100644 --- a/src/chrome/src/agent/agent.js +++ b/src/chrome/src/agent/agent.js @@ -9101,14 +9101,54 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d /** * Decide whether to capture an auto-screenshot after a tool call, based on * the current setting and which tool ran. + * + * Cost model: every auto-screenshot is not just image bytes in the context + * (pruned to the last few messages) — it is also a *separate vision LLM call* + * to describe it. On a run of repetitive actions (invite N people, like N + * posts, click through a list) the default `state_change` mode therefore paid + * one capture + one full model round trip per action, which is the largest + * avoidable token and latency cost in a run. + * + * Two gates keep the informative shots and drop the redundant ones: + * - `opts.domEvidenceProven`: the caller already proved the step's outcome + * from the DOM, so a second, visual copy of the same fact adds nothing. + * - burst throttle: keep the first capture of a burst, then skip while the + * agent keeps acting inside the window. The page state a screenshot would + * show barely changes between consecutive clicks. */ - _shouldAutoScreenshot(toolName) { + _shouldAutoScreenshot(toolName, opts = {}) { const mode = this.autoScreenshot; if (mode === 'off' || !mode) return false; - if (mode === 'every_step') return true; - if (mode === 'state_change') return Agent.STATE_CHANGE_TOOLS.has(toolName); - if (mode === 'navigation') return Agent.NAV_TOOLS.has(toolName); - return false; + if (opts.domEvidenceProven === true) return false; + const isStateChange = Agent.STATE_CHANGE_TOOLS.has(toolName); + const eligible = mode === 'every_step' + || (mode === 'state_change' && isStateChange) + || (mode === 'navigation' && Agent.NAV_TOOLS.has(toolName)); + if (!eligible) return false; + if (mode === 'navigation') return true; + if (Number.isFinite(opts.turnCaptures) && Number.isFinite(this.maxScreenshotsPerTurn) + && this.maxScreenshotsPerTurn > 0 + && opts.turnCaptures >= this.maxScreenshotsPerTurn) { + return false; + } + // Repetition-aware budget. A burst of the SAME action (invite N people, + // like N posts, click through a list) barely moves the page between steps, + // yet the default `state_change` mode paid an image plus a full vision LLM + // call for every one of them — the largest avoidable token and latency cost + // in a run. Capture the first action of a burst, then only every 4th, so a + // long burst still gets periodic visual evidence. A different action resets + // the burst, because that usually does change what is on screen. + // + // Deliberately not time-based: consecutive agent steps are seconds apart + // because each contains an LLM round trip, so an idle-gap throttle would + // never engage. + if (this._autoScreenshotLastTool !== toolName) { + this._autoScreenshotLastTool = toolName; + this._autoScreenshotSameToolStreak = 1; + return true; + } + this._autoScreenshotSameToolStreak = (Number(this._autoScreenshotSameToolStreak) || 1) + 1; + return this._autoScreenshotSameToolStreak % 4 === 1; } /** @@ -23260,12 +23300,28 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d ? this._resolveTwitterNamedMessagingTarget(target, probe) : null; if (resolvedTwitterTarget) target = resolvedTwitterTarget; const messageBodyBaselineCount = Number(probe?.messageBodyBaselineCount); + const targetMatchesObserved = messageTargetMatchesObservedIdentities( + target, + this._messageRecipientCandidates(probe), + ); + // The user's OWN grant authorizes the send even when the planner's + // original target differs — that is the entire point of asking. A bound + // recipient clarification approves one address for the rest of the task, + // and an explicit "send all remaining emails" answer approves the task. + // Both only exist as the result of a real clarify answer, never model text. + const observedIdentities = this._messageRecipientCandidates(probe) + .map(item => normalizeRecipientIdentity(item?.identity || item)) + .filter(Boolean); + const approvedRecipients = Array.isArray(guard?.approvedRecipients) ? guard.approvedRecipients : []; + const approvedByUser = guard?.messageRecipientApprovedAll === true + || (observedIdentities.length > 0 + && observedIdentities.every(identity => approvedRecipients.includes(identity))); const verified = probe?.success === true && probe.messageSend === true && !!this._workflowMessageBody(probe?.messageBody) && Number.isInteger(messageBodyBaselineCount) && messageBodyBaselineCount >= 0 - && messageTargetMatchesObservedIdentities(target, this._messageRecipientCandidates(probe)); + && (targetMatchesObserved || approvedByUser); if (verified) { const binding = probe?.messageRecipientDispatchBinding; if (!binding?.token) { @@ -23291,6 +23347,20 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d executionContext.messageRecipientTwitterEmptyConversationBaseline = true; } } + // The user approved this recipient even though the planner's original + // target did not match, so re-point the guard at what the user agreed + // to. Later sends in the same task then verify against the approval + // list instead of re-asking for the same address. + if (!targetMatchesObserved && approvedByUser && observedIdentities.length > 0) { + const approvedTarget = normalizeMessageTarget({ + target_kind: 'named', + recipients: this._messageRecipientCandidates(probe).map(item => ({ + identity: item?.identity || item, + role: item?.role || 'to', + })), + }); + if (approvedTarget && guard) guard.messaging = approvedTarget; + } if (probe.composerSubjectAvailable === true) { executionContext.messageRecipientSubject = this._workflowMetadataValue(probe.composerSubject); executionContext.messageRecipientSubjectAvailable = true; @@ -23302,6 +23372,35 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d return null; } + // A send-labelled control on a page that exposes NO recipient identity at + // all is a contact/feedback form, not a message with an addressee to + // protect — the recipient guard has nothing to verify there, and blocking + // it would loop the agent on a page that cannot ever answer. Known + // adapters keep their stricter behaviour; on generic surfaces we fail open + // and let the separate submit-confirmation gate govern the submission. + if (probe?.success === true + && probe?.conclusive === true + && probe?.messageSend === true + && observedIdentities.length === 0 + && policy?.adapterName === 'generic-messaging') { + return null; + } + + // Unclassifiable actions pass through. + // + // A recipient guard exists to stop a mis-addressed MESSAGE — it must never + // veto an action the classifier could not place. Blocking these produced + // "could not conclusively resolve the target control and active composer" + // on pages that were perfectly usable, made the agent give up and report a + // blocker, and forced the user to re-confirm forever. Only a CONFIDENT send + // classification reaches the recipient verification below; everything the + // probe could not resolve is left to the normal dispatch path, which + // enforces its own target resolution, permission gate, and submit + // confirmation. + if (probe?.success !== true || probe?.conclusive !== true || probe?.messageSend !== true) { + return null; + } + // Retain what the page actually showed only when the plan already permits // messaging and has pre-existing messaging authorization. The block below // tells the model to ask the user who the message is for, and that answer @@ -23334,22 +23433,47 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d const missingLinkedInComposer = policy.adapterName === 'linkedin' && probe?.success === true && probe?.composerAvailable === false; + const inconclusive = probe?.success !== true || probe?.conclusive !== true || probe?.messageSend !== true; + // Human-readable view of who the page actually addresses, so a mismatch + // block can name the exact observed recipient instead of asking the user to + // re-confirm blindly (which looped forever when the user had already + // confirmed twice: the task authorization, not the chat answer, is what + // the guard compares against). + const observedRecipientLabel = (() => { + try { + return (this._messageRecipientCandidates(probe) || []) + .map(item => String(item?.identity || item?.name || item || '').trim()) + .filter(Boolean) + .slice(0, 3) + .join(', '); + } catch { + return ''; + } + })(); return { success: false, blocked: true, noDispatch: true, dispatched: false, messageRecipientGuard: true, - ...(missingLinkedInComposer ? { retryable: false } : {}), - reasonCode: probe?.success !== true || probe?.conclusive !== true || probe?.messageSend !== true - ? 'message_send_classification_inconclusive' + ...((missingLinkedInComposer || (!inconclusive && target)) ? { retryable: false } : {}), + reasonCode: inconclusive + ? (probe?.reasonCode || 'message_send_classification_inconclusive') : (target ? 'active_recipient_unverified' : 'authorized_recipient_missing'), error: missingLinkedInComposer - ? 'Message action blocked: no message composer is visible and this control is not a verified non-send action. Changing click targeting methods will not resolve this classification failure. Re-read the page to find a supported navigation or composer-opening control; if none is available, report the blocker instead of repeating the click.' - : probe?.success !== true || probe?.conclusive !== true || probe?.messageSend !== true + ? (probe?.reasonCode === 'message_commit_without_composer' + ? 'Message send blocked: this control commits a message (for example "Send" / "Send without a note") but no active composer or recipient could be verified. Changing click targeting methods will not help; ask the user to confirm the recipient or open the intended conversation first.' + : 'Message send blocked: no message composer is visible, so this send cannot be verified. Changing click targeting methods will not resolve this classification failure. Re-read the page to find a supported navigation or composer-opening control; if none is available, report the blocker instead of repeating the click.') + : inconclusive ? 'Message action blocked: WebBrain could not conclusively resolve the target control and active composer. Re-read the page and retry with an exact visible control or fresh ref_id.' : target - ? 'Message send blocked: the active conversation does not exactly match the recipient authorized by the user. Select the intended conversation, re-read its visible header, then retry the send action.' + ? `Message send blocked: this send would go to ${observedRecipientLabel ? `"${observedRecipientLabel}"` : 'a recipient'} which is not the recipient authorized for this task, so nothing was dispatched. Do NOT click Send again, and do not retry with different targeting. +Ask the user to authorize it — one clarify, then act on their answer. The options must carry the address/identity text itself, because authorization binds to what the page shows and a bare "yes" cannot authorize a send. Include the multi-send option when the task sends more than one message: +clarify({ question: "Send to ${observedRecipientLabel || ''} instead?", options: ["${observedRecipientLabel || ''}", "Send all remaining emails in this task without asking again", "Cancel"] }) +- If they pick the address: that recipient is authorized for the rest of this task, so retry the Send and continue normally. +- If they pick "send all remaining": every remaining send in this task is authorized, so keep going without asking again. +- If they pick "Cancel": stop and report that the send was not authorized. +If the user has already named or confirmed this exact recipient, do NOT ask again — treat it as authorized, retry the Send once, and continue.` : (guard?.requiresSubmission === false || guard?.requiresStateChange === false || !target ? 'Message send blocked: the current plan does not authorize sending or submitting messages. Return the draft in chat or ask the user to authorize sending before attempting delivery.' : 'Message send blocked: the current task has no structured recipient authorization. Ask the user to name the recipient or explicitly authorize the currently open conversation before retrying.'), @@ -23447,11 +23571,31 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d } const normalizedAnswer = normalizeRecipientAnswer(answer); if (!normalizedAnswer) return false; - if (!answerNamesAllObservedRecipients(normalizedAnswer, observed)) return false; - const resolvedRecipients = resolveClarifiedRecipients(observed, guard.messaging, clarifyContext, answer); + // A user may authorize the rest of the task in one answer instead of one + // recipient at a time (multi-email / multi-recipient runs). This is still + // the USER's grant: it can only arrive as a real clarify answer, never as + // model text, and it still requires a page-observed recipient to exist. + const allowAllSends = /(?:^|[^\p{L}])(?:all|everything|any|every|allow\s+all|send\s+all)(?:[^\p{L}]|$)/iu + .test(normalizedAnswer); + if (!allowAllSends && !answerNamesAllObservedRecipients(normalizedAnswer, observed)) return false; + const resolvedRecipients = allowAllSends + ? observed + .map(item => ({ identity: item?.identity || item, role: item?.role || 'to' })) + .filter(recipient => recipient.identity) + : resolveClarifiedRecipients(observed, guard.messaging, clarifyContext, answer); const target = normalizeMessageTarget({ target_kind: 'named', recipients: resolvedRecipients }); if (!target) return false; guard.messaging = target; + // Remember the grant for the rest of this task: a multi-email run must not + // re-ask for an address the user already authorized, and an explicit + // "send all remaining" answer covers the sends that come after it. + const approved = Array.isArray(guard.approvedRecipients) ? guard.approvedRecipients.slice() : []; + for (const recipient of target.recipients || []) { + const identity = normalizeRecipientIdentity(recipient?.identity || recipient); + if (identity && !approved.includes(identity)) approved.push(identity); + } + guard.approvedRecipients = approved; + if (allowAllSends) guard.messageRecipientApprovedAll = true; return true; } @@ -39547,6 +39691,48 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d let response; try { response = await dispatchContentAction(); + if (response === undefined || response === null) { + // The content script accepted the message but never answered — the + // classic cause is an orphaned copy left in this tab by a previous + // extension load (reload, update, or a moved folder): its runtime + // context is dead, so it receives messages but cannot respond, and + // it used to block the fresh install's double-injection guard. + // Force-inject the current content scripts (content.js now takes + // over via its generation counter) and retry once. + try { + if (globalThis.chrome?.scripting?.executeScript) { + await chrome.scripting.executeScript({ + target: { tabId }, + files: [ + 'src/content/rich-text-toolbar-heuristic.js', + 'src/content/accessibility-tree.js', + 'src/content/teacher-capture.js', + 'src/content/chat-observation.js', + 'src/content/content.js', + 'src/content/agent-visual-indicator.js', + 'src/content/ollama-launch-handoff.js', + ], + }); + } + } catch { /* best-effort — the resend below decides the outcome */ } + try { + await runContentActionStage(() => this._injectCoreContentScripts(tabId)); + response = await dispatchContentAction(); + } catch (eInj) { + if (eInj?.code === 'content_action_timeout') throw eInj; + response = undefined; + } + if (response === undefined || response === null) { + return { + success: false, + dispatched: false, + noDispatch: true, + retryable: true, + staleContentScript: true, + error: `${name} received no answer from the page. The content script may be stale for this tab (reload the tab, or toggle the extension off/on) or the page blocked script injection. Re-observe the page before retrying.`, + }; + } + } const deadlineResult = provenNoDispatchDeadline(response); if (deadlineResult) return this._withCoordinateReconciliation(deadlineResult, coordinateDiagnostic); } catch (e) { From 0b1913ff436720003485ed3514d84336e3520f61 Mon Sep 17 00:00:00 2001 From: Gaurav Singh <87198156+gaur-avvv@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:26:18 +0530 Subject: [PATCH 4/6] feat(adapters): generic-first recipient guard on any site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recipient verification is armed on any URL, not just Gmail/LinkedIn/Douyin. Content-side recipient extraction decides whether the page is actually sending something — no address and no conversation header means nothing is a send and nothing is blocked. - GENERIC_MESSAGING_SURFACE_RE / GENERIC_MESSAGING_MAIL_LIKE_RE (mail/webmail and chat hosts, plus generic /mail /messages /chat /dm /compose /inbox routes) only select the verification shape: mail-like surfaces accept a To/Cc/Bcc set (supportsRecipientSets), chat-like surfaces keep the strict single-conversation-identity rule. --- src/chrome/src/agent/adapters.js | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/chrome/src/agent/adapters.js b/src/chrome/src/agent/adapters.js index a59b82848..d254b0c25 100644 --- a/src/chrome/src/agent/adapters.js +++ b/src/chrome/src/agent/adapters.js @@ -18187,6 +18187,29 @@ export function getFullPageCapturePolicy(url) { } } +/** + * Messaging / webmail surfaces that have no first-party adapter but still get + * the same recipient verification and confirm-then-send flow. Matched against + * `hostname + pathname`, so it covers both known providers and generic + * messaging routes on any site (/messages, /chat, /dm, /compose, /inbox, ...). + */ +const GENERIC_MESSAGING_SURFACE_RE = new RegExp( + '(?:^|\\.)(?:mail|webmail|outlook|hotmail|yahoo|proton|zoho|fastmail|icloud|gmx|tutanota|roundcube|horde|zimbra)\\b' + + '|(?:^|\\.)(?:whatsapp|telegram|discord|slack|messenger|wechat|signal|skype|teams|element|mattermost)\\.' + + '|/(?:mail|messages?|chats?|dm|direct|inbox|compose|conversations?)(?:/|$)', + 'i', +); +// Mail-like surfaces carry a To/Cc/Bcc set; chat-like surfaces address one +// conversation. Only the verification shape differs — both get the same +// confirm-then-send flow. +const GENERIC_MESSAGING_MAIL_LIKE_RE = new RegExp( + '(?:^|\\.)(?:mail|webmail|outlook|hotmail|yahoo|proton|zoho|fastmail|icloud|gmx|tutanota|roundcube|horde|zimbra)\\b' + + '|/(?:mail|compose|inbox)(?:/|$)', + 'i', +); + + + /** * Return the machine-readable recipient-safety policy for a messaging page. * Unlike adapter notes, this is enforced by the runtime before dispatch. @@ -18200,7 +18223,32 @@ export function getMessageRecipientGuardPolicy(url) { } catch { enabled = false; } - if (!enabled) return null; + if (!enabled) { + // Any page on the web may host a messaging or mail surface. The recipient + // guard is therefore generic-first: arm it everywhere and let the + // content-side recipient extraction decide whether this page is actually + // sending something. If it finds no address and no conversation header, + // nothing is a send and no action is blocked; when it does find one, the + // same verify → ask → authorize flow used by Gmail applies. These entries + // only pick the verification shape (single conversation identity vs a + // To/Cc/Bcc set). + let mailLike = false; + let chatLike = false; + try { + const parsed = new URL(url); + const hostPath = `${parsed.hostname}${parsed.pathname}`; + mailLike = GENERIC_MESSAGING_MAIL_LIKE_RE.test(hostPath); + chatLike = !mailLike && GENERIC_MESSAGING_SURFACE_RE.test(hostPath); + } catch { + mailLike = false; + chatLike = false; + } + return { + adapterName: adapter?.name || 'generic-messaging', + verifyActiveRecipient: true, + ...(mailLike || chatLike ? { supportsRecipientSets: true } : {}), + }; + } return { adapterName: adapter.name, verifyActiveRecipient: true, From 7ca5c5ce6cf1a63da17ff0133cfbe4e263ffcbce Mon Sep 17 00:00:00 2001 From: Gaurav Singh <87198156+gaur-avvv@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:29:27 +0530 Subject: [PATCH 5/6] feat(sidepanel): chime when a clarification/permission card appears - playClarifySound() plays the completion chime asset when renderClarifyCard renders a clarification or permission card, so the user hears immediately when the agent needs input. Respects the existing notify-sound setting and is best-effort (autoplay-blocked browsers just skip it). --- src/chrome/src/ui/sidepanel.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/chrome/src/ui/sidepanel.js b/src/chrome/src/ui/sidepanel.js index 59f68e0c5..bc6e8126d 100644 --- a/src/chrome/src/ui/sidepanel.js +++ b/src/chrome/src/ui/sidepanel.js @@ -1702,6 +1702,22 @@ function playCompletionSound() { } catch { /* ignore */ } } +// Play a short chime when the agent asks the user for clarification or a +// permission decision. Uses the same asset + toggle as the completion chime. +// Best-effort: autoplay-blocked or a closed panel just swallows the error. +function playClarifySound() { + if (!notifySoundEnabled) return; + try { + if (!notifyAudio) { + notifyAudio = new Audio(chrome.runtime.getURL('assets/notification.mp3')); + notifyAudio.volume = 0.6; + } + notifyAudio.currentTime = 0; + const p = notifyAudio.play(); + if (p && typeof p.catch === 'function') p.catch(() => {}); + } catch { /* ignore */ } +} + function triggerCompletionConfetti() { if (!completionConfettiEnabled) return; if (globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) return; @@ -10115,6 +10131,8 @@ function workflowHealingTargetLabel(target) { function renderClarifyCard(data) { hideActivity(); + // A clarification / permission card needs the user's attention right now. + playClarifySound(); const tabId = data?.scheduledTabId ?? data?.tabId ?? currentTabId; if (tabId == null) return; const scheduledJobId = data?.scheduledJobId ? String(data.scheduledJobId) : ''; From adfb622f22d73b666f7ed6a64dc519883251e94a Mon Sep 17 00:00:00 2001 From: release-verification Date: Mon, 21 Sep 2026 01:35:26 +0300 Subject: [PATCH 6/6] fix: address recipient guard review findings --- src/chrome/src/agent/adapters.js | 35 +-- src/chrome/src/agent/agent.js | 79 +++---- src/chrome/src/content/content.js | 61 ++--- src/firefox/src/agent/adapters.js | 31 ++- src/firefox/src/agent/agent.js | 112 +++++++++- src/firefox/src/content/content.js | 344 +++++++++++++++++++++++++++-- src/firefox/src/ui/sidepanel.js | 7 + test/run.js | 72 +++++- 8 files changed, 594 insertions(+), 147 deletions(-) diff --git a/src/chrome/src/agent/adapters.js b/src/chrome/src/agent/adapters.js index d254b0c25..d2c9c6a68 100644 --- a/src/chrome/src/agent/adapters.js +++ b/src/chrome/src/agent/adapters.js @@ -18187,29 +18187,20 @@ export function getFullPageCapturePolicy(url) { } } -/** - * Messaging / webmail surfaces that have no first-party adapter but still get - * the same recipient verification and confirm-then-send flow. Matched against - * `hostname + pathname`, so it covers both known providers and generic - * messaging routes on any site (/messages, /chat, /dm, /compose, /inbox, ...). - */ +// Generic mail routes may legitimately show a To/Cc/Bcc set. Generic chat +// routes must resolve to one visible conversation identity before dispatch. const GENERIC_MESSAGING_SURFACE_RE = new RegExp( '(?:^|\\.)(?:mail|webmail|outlook|hotmail|yahoo|proton|zoho|fastmail|icloud|gmx|tutanota|roundcube|horde|zimbra)\\b' + '|(?:^|\\.)(?:whatsapp|telegram|discord|slack|messenger|wechat|signal|skype|teams|element|mattermost)\\.' - + '|/(?:mail|messages?|chats?|dm|direct|inbox|compose|conversations?)(?:/|$)', + + '|/(?:mail|messag(?:e|es|ing)|chats?|dm|direct|compose|conversations?)(?:/|$)', 'i', ); -// Mail-like surfaces carry a To/Cc/Bcc set; chat-like surfaces address one -// conversation. Only the verification shape differs — both get the same -// confirm-then-send flow. const GENERIC_MESSAGING_MAIL_LIKE_RE = new RegExp( '(?:^|\\.)(?:mail|webmail|outlook|hotmail|yahoo|proton|zoho|fastmail|icloud|gmx|tutanota|roundcube|horde|zimbra)\\b' - + '|/(?:mail|compose|inbox)(?:/|$)', + + '|/(?:mail|compose)(?:/|$)', 'i', ); - - /** * Return the machine-readable recipient-safety policy for a messaging page. * Unlike adapter notes, this is enforced by the runtime before dispatch. @@ -18224,14 +18215,6 @@ export function getMessageRecipientGuardPolicy(url) { enabled = false; } if (!enabled) { - // Any page on the web may host a messaging or mail surface. The recipient - // guard is therefore generic-first: arm it everywhere and let the - // content-side recipient extraction decide whether this page is actually - // sending something. If it finds no address and no conversation header, - // nothing is a send and no action is blocked; when it does find one, the - // same verify → ask → authorize flow used by Gmail applies. These entries - // only pick the verification shape (single conversation identity vs a - // To/Cc/Bcc set). let mailLike = false; let chatLike = false; try { @@ -18239,14 +18222,12 @@ export function getMessageRecipientGuardPolicy(url) { const hostPath = `${parsed.hostname}${parsed.pathname}`; mailLike = GENERIC_MESSAGING_MAIL_LIKE_RE.test(hostPath); chatLike = !mailLike && GENERIC_MESSAGING_SURFACE_RE.test(hostPath); - } catch { - mailLike = false; - chatLike = false; - } + } catch {} + if (!mailLike && !chatLike) return null; return { - adapterName: adapter?.name || 'generic-messaging', + adapterName: 'generic-messaging', verifyActiveRecipient: true, - ...(mailLike || chatLike ? { supportsRecipientSets: true } : {}), + ...(mailLike ? { supportsRecipientSets: true } : {}), }; } return { diff --git a/src/chrome/src/agent/agent.js b/src/chrome/src/agent/agent.js index 064149735..03864d006 100644 --- a/src/chrome/src/agent/agent.js +++ b/src/chrome/src/agent/agent.js @@ -1080,6 +1080,9 @@ export class Agent extends LoopDetector { this.deliveryObservationStreaks = new Map(); // tabId -> count this.deliveryActionableDiscoveryResets = new Set(); // tabIds that used their one discovery reset since meaningful progress this.lastAutoScreenshotTs = new Map(); // tabId -> ms — defensive debounce + // tabId -> { toolName, streak }. Burst state must not leak across tabs or + // be consumed by read-only preflight checks. + this.autoScreenshotBursts = new Map(); this.lastSeenAdapter = new Map(); // tabId -> adapter name from last enrichment this.pendingAdapterMatchTraces = new Map(); // tabId -> Map(adapter@revision -> content-free match metadata) this.adapterMatchTraceKeys = new Map(); // runId -> Map(adapter@revision -> OR-merged match metadata) @@ -9131,24 +9134,19 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d && opts.turnCaptures >= this.maxScreenshotsPerTurn) { return false; } - // Repetition-aware budget. A burst of the SAME action (invite N people, - // like N posts, click through a list) barely moves the page between steps, - // yet the default `state_change` mode paid an image plus a full vision LLM - // call for every one of them — the largest avoidable token and latency cost - // in a run. Capture the first action of a burst, then only every 4th, so a - // long burst still gets periodic visual evidence. A different action resets - // the burst, because that usually does change what is on screen. - // - // Deliberately not time-based: consecutive agent steps are seconds apart - // because each contains an LLM round trip, so an idle-gap throttle would - // never engage. - if (this._autoScreenshotLastTool !== toolName) { - this._autoScreenshotLastTool = toolName; - this._autoScreenshotSameToolStreak = 1; - return true; - } - this._autoScreenshotSameToolStreak = (Number(this._autoScreenshotSameToolStreak) || 1) + 1; - return this._autoScreenshotSameToolStreak % 4 === 1; + return true; + } + + // Advance the burst only after a successful post-dispatch capture request. + // _shouldAutoScreenshot is used by preflight, so it must remain pure. + _advanceAutoScreenshotBurst(tabId, toolName) { + if (this.autoScreenshot === 'navigation') return true; + const normalizedTool = String(toolName || ''); + if (!normalizedTool) return true; + const prior = this.autoScreenshotBursts.get(tabId); + const streak = prior?.toolName === normalizedTool ? prior.streak + 1 : 1; + this.autoScreenshotBursts.set(tabId, { toolName: normalizedTool, streak }); + return streak === 1 || streak % 4 === 1; } /** @@ -11598,6 +11596,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d async _executeToolBatchInner(tabId, toolCalls, messages, onUpdate, provider, partialAssistantText, allowedToolNames, step, runOptions, toolSchemas, cancellationState) { let didStateChange = false; + let autoScreenshotToolName = ''; const apiMutationsDeniedForRun = runOptions.apiMutationsDenied === true; const apiMutationsAllowedForRun = () => !apiMutationsDeniedForRun && this.isApiMutationsAllowed(tabId); @@ -13572,7 +13571,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d // The interruption helper already emitted the queued synthetic results // and navigation notice. Continue through the shared post-batch path so // state_change/every_step auto-screenshots still reach the fresh turn. - if (this._shouldAutoScreenshot(fnName) && !toolResult?.error) didStateChange = true; + if (this._shouldAutoScreenshot(fnName) && !toolResult?.error) { + didStateChange = true; + autoScreenshotToolName = fnName; + } navNotices.length = 0; break; } @@ -13622,6 +13624,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d if (this._shouldAutoScreenshot(fnName) && !toolResult?.error) { didStateChange = true; + autoScreenshotToolName = fnName; } } @@ -13632,12 +13635,14 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d // Auto-screenshot once per batch, debounced 500ms. Capture if either // the main provider supports images, or a dedicated vision model is // configured to describe them. + const captureAfterBatch = didStateChange + && this._advanceAutoScreenshotBurst(tabId, autoScreenshotToolName); const visionRoute = await this._resolveVisionRoute(tabId, provider); - if (didStateChange && !visionRoute.provider && visionRoute.visionStatus) { + if (captureAfterBatch && !visionRoute.provider && visionRoute.visionStatus) { this._recordVisionRouteTrace(tabId, visionRoute, null, 'auto_screenshot'); this._emitVisionUnavailableNotice(tabId, visionRoute, onUpdate, 'Automatic screenshot'); } - if (didStateChange && visionRoute.provider) { + if (captureAfterBatch && visionRoute.provider) { const lastTs = this.lastAutoScreenshotTs.get(tabId) || 0; if (Date.now() - lastTs >= 500) { await new Promise(r => setTimeout(r, 250)); @@ -23372,35 +23377,6 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d return null; } - // A send-labelled control on a page that exposes NO recipient identity at - // all is a contact/feedback form, not a message with an addressee to - // protect — the recipient guard has nothing to verify there, and blocking - // it would loop the agent on a page that cannot ever answer. Known - // adapters keep their stricter behaviour; on generic surfaces we fail open - // and let the separate submit-confirmation gate govern the submission. - if (probe?.success === true - && probe?.conclusive === true - && probe?.messageSend === true - && observedIdentities.length === 0 - && policy?.adapterName === 'generic-messaging') { - return null; - } - - // Unclassifiable actions pass through. - // - // A recipient guard exists to stop a mis-addressed MESSAGE — it must never - // veto an action the classifier could not place. Blocking these produced - // "could not conclusively resolve the target control and active composer" - // on pages that were perfectly usable, made the agent give up and report a - // blocker, and forced the user to re-confirm forever. Only a CONFIDENT send - // classification reaches the recipient verification below; everything the - // probe could not resolve is left to the normal dispatch path, which - // enforces its own target resolution, permission gate, and submit - // confirmation. - if (probe?.success !== true || probe?.conclusive !== true || probe?.messageSend !== true) { - return null; - } - // Retain what the page actually showed only when the plan already permits // messaging and has pre-existing messaging authorization. The block below // tells the model to ask the user who the message is for, and that answer @@ -25958,6 +25934,7 @@ If the user has already named or confirmed this exact recipient, do NOT ask agai this._continuationResponseLanguagePolicies.delete(tabId); this.mastodonStates.delete(tabId); this.lastAutoScreenshotTs.delete(tabId); + this.autoScreenshotBursts.delete(tabId); this.autoScreenshotCount.delete(tabId); this.toolbarAuditScreenshotCount.delete(tabId); this.toolbarAuditBudgetNotified.delete(tabId); @@ -41725,6 +41702,7 @@ If the user has already named or confirmed this exact recipient, do NOT ask agai this.pendingVisionSubCallTraces.delete(tabId); this.visionStatusNotices.delete(tabId); // Reset the per-turn auto-screenshot budget (issue #311) for a fresh turn. + this.autoScreenshotBursts.delete(tabId); this.autoScreenshotCount.delete(tabId); this.toolbarAuditScreenshotCount.delete(tabId); this.toolbarAuditBudgetNotified.delete(tabId); @@ -43132,6 +43110,7 @@ If the user has already named or confirmed this exact recipient, do NOT ask agai this.pendingVisionSubCallTraces.delete(tabId); this.visionStatusNotices.delete(tabId); // Reset the per-turn auto-screenshot budget (issue #311) for a fresh turn. + this.autoScreenshotBursts.delete(tabId); this.autoScreenshotCount.delete(tabId); this.toolbarAuditScreenshotCount.delete(tabId); this.toolbarAuditBudgetNotified.delete(tabId); diff --git a/src/chrome/src/content/content.js b/src/chrome/src/content/content.js index 2f71e4248..82d28f8ff 100644 --- a/src/chrome/src/content/content.js +++ b/src/chrome/src/content/content.js @@ -1390,12 +1390,14 @@ // (including every shadow host) and reads innerText per match. clickElement, // the recipient preflight, and every retry all call it. Cache the result and // reuse it until the DOM actually changes. - const _domRevision = { value: 0, observer: null }; - function _ensureDomRevisionObserver() { - if (_domRevision.observer || typeof MutationObserver === 'undefined') return; + const _domRevision = { value: 0, observer: null, observedRoots: new WeakSet() }; + function _observeDomRevisionRoot(root) { + if (!root || _domRevision.observedRoots.has(root) || typeof MutationObserver === 'undefined') return; try { - _domRevision.observer = new MutationObserver(() => { _domRevision.value += 1; }); - _domRevision.observer.observe(document, { + if (!_domRevision.observer) { + _domRevision.observer = new MutationObserver(() => { _domRevision.value += 1; }); + } + _domRevision.observer.observe(root, { childList: true, subtree: true, attributes: true, @@ -1407,11 +1409,17 @@ 'class', 'style', 'role', 'tabindex', ], }); + _domRevision.observedRoots.add(root); } catch { - _domRevision.observer = null; + // Some roots cannot be observed. The uncached caller still retains the + // original click behaviour; do not prevent other roots from registering. } } + function _ensureDomRevisionObserver() { + _observeDomRevisionRoot(document); + } + const _textCandidateCache = new WeakMap(); function _clickTextCandidates(scope) { _ensureDomRevisionObserver(); @@ -1432,6 +1440,7 @@ // labels across roots, without searching outside the selected modal. const candidates = []; const visit = (root) => { + _observeDomRevisionRoot(root); candidates.push(...root.querySelectorAll(selectors)); if (root.shadowRoot) visit(root.shadowRoot); for (const host of root.querySelectorAll('*')) { @@ -1440,11 +1449,10 @@ }; visit(cacheScope); const out = []; - const seen = new Set(); for (const e of candidates) { if (!_hasVisibleBox(e)) continue; const text = _siteInteractionText(e).toLowerCase(); - if (text && !seen.has(`${text}\u0000${out.length}`)) out.push({ e, txt: text }); + if (text) out.push({ e, txt: text }); // A control's aria-label / title / placeholder often names it far better // than its visible glyphs — icon-only buttons and combobox-style fields // ("Search for contacts") are unreachable by visible text alone. Offer @@ -5165,7 +5173,8 @@ ).replace(/\s+/g, ' ').trim(); if (label && _COMPOSER_UTILITY_LABEL_RE.test(label)) return true; // Formatting toolbars and pressed toggles are structural non-sends. - if (el.closest?.('[role="toolbar"],[aria-label*="formatting" i],[aria-label*="format" i]')) return true; + const formattingScope = el.closest?.('[role="toolbar"],[aria-label*="formatting" i],[aria-label*="format" i]'); + if (formattingScope && formattingScope !== el) return true; if (el.hasAttribute?.('aria-pressed')) return true; return false; } catch { @@ -5855,32 +5864,6 @@ const verticalGap = Math.max(0, composerRect.top - controlRect.bottom, controlRect.top - composerRect.bottom); messageSend = sameForm || (horizontalGap <= 240 && verticalGap <= 120); if (!messageSend) { - // The control is geometrically far from the composer and not in the - // same form, so clicking it cannot commit the message. Geometry alone - // is enough evidence — but only after also ruling out a send-labelled - // control and anything inside the app's messaging surface, both of - // which can commit without sitting near the composer. That second - // check is what makes far-away controls self-healing instead of - // being blocked as "inconclusive" and retried in a loop. - const farLabel = compact( - control.getAttribute?.('aria-label') - || control.getAttribute?.('title') - || control.getAttribute?.('data-tooltip') - || control.value - || control.innerText - || control.textContent, - 120, - ); - if (!_hasMessageCommitName(farLabel) && !_inMessagingSurface(control)) { - return { - success: true, - messageSend: false, - conclusive: true, - nonMessagingTarget: true, - reasonCode: 'non_messaging_target', - identityCandidates: [], - }; - } return { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; } } @@ -6220,6 +6203,8 @@ // depending on one vendor's markup. This is what makes confirm-then-send // work globally rather than only on Gmail. if (observedRecipientCandidates.length === 0) { + const genericMailRecipientMode = params.adapterName === 'generic-messaging' + && params.supportsRecipientSets === true; const RECIPIENT_FIELD_RE = new RegExp( '(?:^|[^\\p{L}])(?:to|to\\s+recipients?|recipients?|send\\s+to|email|e-?mail(?:\\s+address)?' + '|destinatario|destinataria|destinataire|para|aan|kime|do|til|komu|\\u0644\\u0625\\u0649' @@ -6228,8 +6213,8 @@ ); const fieldLooksLikeRecipient = (el) => { try { - if (/^email$/i.test(String(el.getAttribute?.('type') || ''))) return true; - if (/^email$/i.test(String(el.getAttribute?.('autocomplete') || ''))) return true; + if (/^email$/i.test(String(el.getAttribute?.('type') || ''))) return genericMailRecipientMode; + if (/^email$/i.test(String(el.getAttribute?.('autocomplete') || ''))) return genericMailRecipientMode; const haystack = [ el.getAttribute?.('name'), el.getAttribute?.('aria-label'), @@ -6290,6 +6275,8 @@ '[email],[data-email],[data-hovercard-id],a[href^="mailto:" i],input[type="email"]', )).slice(0, 40)) { if (!visible(el) || el === composer || composer?.contains?.(el)) continue; + if (!genericMailRecipientMode + && /^email$/i.test(String(el.getAttribute?.('type') || ''))) continue; const mailto = String(el.getAttribute?.('href') || '') .replace(/^mailto:/i, '').split('?')[0]; const address = [ diff --git a/src/firefox/src/agent/adapters.js b/src/firefox/src/agent/adapters.js index ecf214489..650ed7ecd 100644 --- a/src/firefox/src/agent/adapters.js +++ b/src/firefox/src/agent/adapters.js @@ -18185,6 +18185,20 @@ export function getFullPageCapturePolicy(url) { } } +// Generic mail routes may legitimately show a To/Cc/Bcc set. Generic chat +// routes must resolve to one visible conversation identity before dispatch. +const GENERIC_MESSAGING_SURFACE_RE = new RegExp( + '(?:^|\\.)(?:mail|webmail|outlook|hotmail|yahoo|proton|zoho|fastmail|icloud|gmx|tutanota|roundcube|horde|zimbra)\\b' + + '|(?:^|\\.)(?:whatsapp|telegram|discord|slack|messenger|wechat|signal|skype|teams|element|mattermost)\\.' + + '|/(?:mail|messag(?:e|es|ing)|chats?|dm|direct|compose|conversations?)(?:/|$)', + 'i', +); +const GENERIC_MESSAGING_MAIL_LIKE_RE = new RegExp( + '(?:^|\\.)(?:mail|webmail|outlook|hotmail|yahoo|proton|zoho|fastmail|icloud|gmx|tutanota|roundcube|horde|zimbra)\\b' + + '|/(?:mail|compose)(?:/|$)', + 'i', +); + /** * Return the machine-readable recipient-safety policy for a messaging page. * Unlike adapter notes, this is enforced by the runtime before dispatch. @@ -18198,7 +18212,22 @@ export function getMessageRecipientGuardPolicy(url) { } catch { enabled = false; } - if (!enabled) return null; + if (!enabled) { + let mailLike = false; + let chatLike = false; + try { + const parsed = new URL(url); + const hostPath = `${parsed.hostname}${parsed.pathname}`; + mailLike = GENERIC_MESSAGING_MAIL_LIKE_RE.test(hostPath); + chatLike = !mailLike && GENERIC_MESSAGING_SURFACE_RE.test(hostPath); + } catch {} + if (!mailLike && !chatLike) return null; + return { + adapterName: 'generic-messaging', + verifyActiveRecipient: true, + ...(mailLike ? { supportsRecipientSets: true } : {}), + }; + } return { adapterName: adapter.name, verifyActiveRecipient: true, diff --git a/src/firefox/src/agent/agent.js b/src/firefox/src/agent/agent.js index 01678fad6..1da212720 100644 --- a/src/firefox/src/agent/agent.js +++ b/src/firefox/src/agent/agent.js @@ -902,6 +902,9 @@ export class Agent extends LoopDetector { // default — loaded from browser.storage.local in background.js. this.screenshotRedaction = false; this.lastAutoScreenshotTs = new Map(); + // tabId -> { toolName, streak } for post-dispatch screenshot throttling. + // It is deliberately independent per tab and reset for every run. + this.autoScreenshotBursts = new Map(); this.lastSeenAdapter = new Map(); this.pendingAdapterMatchTraces = new Map(); // tabId -> Map(adapter@revision -> content-free match metadata) this.adapterMatchTraceKeys = new Map(); // runId -> Map(adapter@revision -> OR-merged match metadata) @@ -8672,13 +8675,35 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d return { imageBlockCount, documentBlockCount }; } - _shouldAutoScreenshot(toolName) { + _shouldAutoScreenshot(toolName, opts = {}) { const mode = this.autoScreenshot; if (mode === 'off' || !mode) return false; - if (mode === 'every_step') return true; - if (mode === 'state_change') return Agent.STATE_CHANGE_TOOLS.has(toolName); - if (mode === 'navigation') return Agent.NAV_TOOLS.has(toolName); - return false; + if (opts.domEvidenceProven === true) return false; + const isStateChange = Agent.STATE_CHANGE_TOOLS.has(toolName); + const eligible = mode === 'every_step' + || (mode === 'state_change' && isStateChange) + || (mode === 'navigation' && Agent.NAV_TOOLS.has(toolName)); + if (!eligible) return false; + if (mode === 'navigation') return true; + if (Number.isFinite(opts.turnCaptures) && Number.isFinite(this.maxScreenshotsPerTurn) + && this.maxScreenshotsPerTurn > 0 + && opts.turnCaptures >= this.maxScreenshotsPerTurn) { + return false; + } + return true; + } + + // Advance only after a successful action batch has requested a screenshot. + // _shouldAutoScreenshot is also used by preflight, so it must remain a pure + // predicate and cannot consume a capture slot or burst position. + _advanceAutoScreenshotBurst(tabId, toolName) { + if (this.autoScreenshot === 'navigation') return true; + const normalizedTool = String(toolName || ''); + if (!normalizedTool) return true; + const prior = this.autoScreenshotBursts.get(tabId); + const streak = prior?.toolName === normalizedTool ? prior.streak + 1 : 1; + this.autoScreenshotBursts.set(tabId, { toolName: normalizedTool, streak }); + return streak === 1 || streak % 4 === 1; } async _currentUrl(tabId) { @@ -10180,6 +10205,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d async _executeToolBatchInner(tabId, toolCalls, messages, onUpdate, provider, partialAssistantText, allowedToolNames, step, runOptions, toolSchemas, cancellationState) { let didStateChange = false; + let autoScreenshotToolName = ''; const apiMutationsDeniedForRun = runOptions.apiMutationsDenied === true; const apiMutationsAllowedForRun = () => !apiMutationsDeniedForRun && this.isApiMutationsAllowed(tabId); @@ -11984,7 +12010,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d // The interruption helper already emitted the queued synthetic results // and navigation notice. Continue through the shared post-batch path so // state_change/every_step auto-screenshots still reach the fresh turn. - if (this._shouldAutoScreenshot(fnName) && !toolResult?.error) didStateChange = true; + if (this._shouldAutoScreenshot(fnName) && !toolResult?.error) { + didStateChange = true; + autoScreenshotToolName = fnName; + } navNotices.length = 0; break; } @@ -12032,6 +12061,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d } if (this._shouldAutoScreenshot(fnName) && !toolResult?.error) { didStateChange = true; + autoScreenshotToolName = fnName; } } @@ -12040,8 +12070,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d // Auto-screenshot after state change. Capture if either the main // provider supports images, or a dedicated vision model is configured // to describe them. + const captureAfterBatch = didStateChange + && this._advanceAutoScreenshotBurst(tabId, autoScreenshotToolName); const visionRoute = await this._resolveVisionRoute(tabId, provider); - if (didStateChange && visionRoute.provider) { + if (captureAfterBatch && visionRoute.provider) { const lastTs = this.lastAutoScreenshotTs.get(tabId) || 0; if (Date.now() - lastTs >= 500) { await new Promise(r => setTimeout(r, 250)); @@ -21019,12 +21051,23 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d ? this._resolveTwitterNamedMessagingTarget(target, probe) : null; if (resolvedTwitterTarget) target = resolvedTwitterTarget; const messageBodyBaselineCount = Number(probe?.messageBodyBaselineCount); + const targetMatchesObserved = messageTargetMatchesObservedIdentities( + target, + this._messageRecipientCandidates(probe), + ); + const observedIdentities = this._messageRecipientCandidates(probe) + .map(item => normalizeRecipientIdentity(item?.identity || item)) + .filter(Boolean); + const approvedRecipients = Array.isArray(guard?.approvedRecipients) ? guard.approvedRecipients : []; + const approvedByUser = guard?.messageRecipientApprovedAll === true + || (observedIdentities.length > 0 + && observedIdentities.every(identity => approvedRecipients.includes(identity))); const verified = probe?.success === true && probe.messageSend === true && !!this._workflowMessageBody(probe?.messageBody) && Number.isInteger(messageBodyBaselineCount) && messageBodyBaselineCount >= 0 - && messageTargetMatchesObservedIdentities(target, this._messageRecipientCandidates(probe)); + && (targetMatchesObserved || approvedByUser); if (verified) { const binding = probe?.messageRecipientDispatchBinding; if (!binding?.token) { @@ -21050,6 +21093,16 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d executionContext.messageRecipientTwitterEmptyConversationBaseline = true; } } + if (!targetMatchesObserved && approvedByUser && observedIdentities.length > 0) { + const approvedTarget = normalizeMessageTarget({ + target_kind: 'named', + recipients: this._messageRecipientCandidates(probe).map(item => ({ + identity: item?.identity || item, + role: item?.role || 'to', + })), + }); + if (approvedTarget && guard) guard.messaging = approvedTarget; + } if (probe.composerSubjectAvailable === true) { executionContext.messageRecipientSubject = this._workflowMetadataValue(probe.composerSubject); executionContext.messageRecipientSubjectAvailable = true; @@ -21206,11 +21259,24 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d } const normalizedAnswer = normalizeRecipientAnswer(answer); if (!normalizedAnswer) return false; - if (!answerNamesAllObservedRecipients(normalizedAnswer, observed)) return false; - const resolvedRecipients = resolveClarifiedRecipients(observed, guard.messaging, clarifyContext, answer); + const allowAllSends = /(?:^|[^\p{L}])(?:all|everything|any|every|allow\s+all|send\s+all)(?:[^\p{L}]|$)/iu + .test(normalizedAnswer); + if (!allowAllSends && !answerNamesAllObservedRecipients(normalizedAnswer, observed)) return false; + const resolvedRecipients = allowAllSends + ? observed + .map(item => ({ identity: item?.identity || item, role: item?.role || 'to' })) + .filter(recipient => recipient.identity) + : resolveClarifiedRecipients(observed, guard.messaging, clarifyContext, answer); const target = normalizeMessageTarget({ target_kind: 'named', recipients: resolvedRecipients }); if (!target) return false; guard.messaging = target; + const approved = Array.isArray(guard.approvedRecipients) ? guard.approvedRecipients.slice() : []; + for (const recipient of target.recipients || []) { + const identity = normalizeRecipientIdentity(recipient?.identity || recipient); + if (identity && !approved.includes(identity)) approved.push(identity); + } + guard.approvedRecipients = approved; + if (allowAllSends) guard.messageRecipientApprovedAll = true; return true; } @@ -24394,6 +24460,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d this._standaloneChatRunTabs.delete(tabId); this.mastodonStates.delete(tabId); this.lastAutoScreenshotTs.delete(tabId); + this.autoScreenshotBursts.delete(tabId); this.autoScreenshotCount.delete(tabId); this.toolbarAuditScreenshotCount.delete(tabId); this.toolbarAuditBudgetNotified.delete(tabId); @@ -34019,6 +34086,29 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d }; try { let response = await dispatchContentAction(); + if (response === undefined || response === null) { + // An orphaned content script can still receive the message but be + // unable to reply after an extension reload or update. Reinject the + // current bundle once so its generation-aware startup can replace the + // stale copy, then require a definite response before continuing. + try { + await runContentActionStage(() => this._injectCoreContentScripts(tabId)); + response = await dispatchContentAction(); + } catch (eInj) { + if (eInj?.code === 'content_action_timeout') throw eInj; + response = undefined; + } + if (response === undefined || response === null) { + return this._withCoordinateReconciliation({ + success: false, + dispatched: false, + noDispatch: true, + retryable: true, + staleContentScript: true, + error: `${name} received no answer from the page. The content script may be stale for this tab (reload the tab, or toggle the extension off/on) or the page blocked script injection. Re-observe the page before retrying.`, + }, coordinateDiagnostic); + } + } const deadlineResult = provenNoDispatchDeadline(response); if (deadlineResult) return this._withCoordinateReconciliation(deadlineResult, coordinateDiagnostic); if (name === 'click') { @@ -34546,6 +34636,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d this.pendingVisionRouteTraces.delete(tabId); this.pendingVisionSubCallTraces.delete(tabId); // Reset the per-turn auto-screenshot budget (issue #311) for a fresh turn. + this.autoScreenshotBursts.delete(tabId); this.autoScreenshotCount.delete(tabId); this.toolbarAuditScreenshotCount.delete(tabId); this.toolbarAuditBudgetNotified.delete(tabId); @@ -35782,6 +35873,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d this.pendingVisionRouteTraces.delete(tabId); this.pendingVisionSubCallTraces.delete(tabId); // Reset the per-turn auto-screenshot budget (issue #311) for a fresh turn. + this.autoScreenshotBursts.delete(tabId); this.autoScreenshotCount.delete(tabId); this.toolbarAuditScreenshotCount.delete(tabId); this.toolbarAuditBudgetNotified.delete(tabId); diff --git a/src/firefox/src/content/content.js b/src/firefox/src/content/content.js index 1ef8a9633..06bf530be 100644 --- a/src/firefox/src/content/content.js +++ b/src/firefox/src/content/content.js @@ -4,8 +4,11 @@ */ (() => { - // Prevent double-injection - if (window.__webbrain_injected) return; + // Take over from an orphaned installation. A reload or update leaves the + // old content script in the page with a dead runtime context; the newest + // copy must be the only listener that answers subsequent tool calls. + window.__webbrain_generation = (Number(window.__webbrain_generation) || 0) + 1; + const WEBBRAIN_GENERATION = window.__webbrain_generation; window.__webbrain_injected = true; const PAGE_GATE_SELECTORS = [ @@ -470,7 +473,8 @@ const descriptor = window.__wbSiteInteractions?.describe?.(el); if (descriptor?.name) return descriptor.name; } catch {} - return (el?.innerText || el?.value || el?.placeholder || el?.title || el?.ariaLabel || '').trim(); + const aria = el?.getAttribute?.('aria-label') || el?.ariaLabel || ''; + return (el?.innerText || el?.value || el?.placeholder || el?.title || aria || '').trim(); } function _isSiteInteractive(el) { @@ -1307,12 +1311,43 @@ // Shared by click dispatch and its recipient-safety preflight. Keep the // Firefox option visibility and input-label rules identical in both paths. + // Text candidate discovery walks every open shadow root, so each discovered + // root must invalidate the cache when its own subtree changes. + const _domRevision = { value: 0, observer: null, observedRoots: new WeakSet() }; + function _observeDomRevisionRoot(root) { + if (!root || _domRevision.observedRoots.has(root) || typeof MutationObserver === 'undefined') return; + try { + if (!_domRevision.observer) { + _domRevision.observer = new MutationObserver(() => { _domRevision.value += 1; }); + } + _domRevision.observer.observe(root, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: [ + 'aria-label', 'aria-labelledby', 'title', 'placeholder', 'value', 'disabled', + 'aria-expanded', 'aria-selected', 'aria-checked', 'aria-hidden', 'hidden', + 'class', 'style', 'role', 'tabindex', + ], + }); + _domRevision.observedRoots.add(root); + } catch { /* leave other observable roots cacheable */ } + } + + const _textCandidateCache = new WeakMap(); function _clickTextCandidates(scope) { + _observeDomRevisionRoot(document); + const revision = _domRevision.value; + const cacheScope = scope && typeof scope === 'object' ? scope : document; + const cached = _textCandidateCache.get(cacheScope); + if (cached && cached.revision === revision) return cached.list.slice(); const sels = [ 'a', 'button', '[role="button"]', '[role="link"]', '[role="tab"]', '[role="menuitem"]', '[role="option"]', '[role="menuitemradio"]', '[role="menuitemcheckbox"]', '[role="treeitem"]', + '[role="combobox"]', '[role="searchbox"]', '[role="textbox"]', 'input:not([type="hidden"])', 'textarea', 'select', 'input[type="button"]', 'input[type="submit"]', 'summary', 'label', '[onclick]', '[data-action]', + '[aria-label]', '[tabindex]', '[data-tooltip]', ..._siteInteractiveSelectors(), ].join(', '); // Candidate filter: listbox/menu option roles are often kept mounted but @@ -1347,22 +1382,34 @@ }; const _normTxt = (el) => { const siteText = _isSiteInteractive(el) ? _siteInteractionText(el) : ''; - return (siteText || el.innerText || (_valIsLabel(el) ? el.value : '') || el.placeholder || el.ariaLabel || '').trim().toLowerCase(); + return (siteText || el.innerText || (_valIsLabel(el) ? el.value : '') || el.placeholder + || el.getAttribute?.('aria-label') || el.ariaLabel || el.title || '').trim().toLowerCase(); }; // LinkedIn can render the feed itself inside an open shadow root. Keep // preflight and dispatch on the same candidate set, including duplicate // labels across roots, without searching outside the selected modal. const candidates = []; const visit = (root) => { + _observeDomRevisionRoot(root); candidates.push(...root.querySelectorAll(sels)); if (root.shadowRoot) visit(root.shadowRoot); for (const host of root.querySelectorAll('*')) { if (host.shadowRoot) visit(host.shadowRoot); } }; - visit(scope); + visit(cacheScope); const all = candidates.filter(e => _hasVisibleBox(e) && _keepCandidate(e)); - return all.map(e => ({ e, txt: _normTxt(e) })).filter(x => !!x.txt); + const list = []; + for (const e of all) { + const text = _normTxt(e); + if (text) list.push({ e, txt: text }); + for (const attr of ['aria-label', 'title', 'placeholder', 'data-tooltip']) { + const value = String(e.getAttribute?.(attr) || '').trim().toLowerCase(); + if (value && value !== text) list.push({ e, txt: value }); + } + } + _textCandidateCache.set(cacheScope, { revision, list }); + return list.slice(); } let _lastClickIdent = null; @@ -1711,7 +1758,14 @@ function tryMode(mode) { if (mode === 'exact') return normalized.filter(x => x.txt === needle); if (mode === 'prefix') return normalized.filter(x => x.txt.startsWith(needle)); - if (mode === 'contains') return normalized.filter(x => x.txt.includes(needle)); + if (mode === 'contains') { + if (needle.length >= 3) return normalized.filter(x => x.txt.includes(needle)); + const padded = ` ${needle} `; + return normalized.filter(x => x.txt === needle + || x.txt.startsWith(`${needle} `) + || x.txt.endsWith(` ${needle}`) + || x.txt.includes(padded)); + } return []; } @@ -1844,12 +1898,49 @@ } } } + // Labels nested inside their actionable control are not independent + // targets. Collapse them before reporting an ambiguity so a single + // visible Send/Post control remains directly clickable. + if (!el && matches.length > 1) { + const collapsedByAncestor = new Map(); + for (const match of matches) { + const actionable = _resolveInteractiveAncestor(match.e) || match.e; + if (!collapsedByAncestor.has(actionable)) collapsedByAncestor.set(actionable, match); + } + if (collapsedByAncestor.size < matches.length) matches = [...collapsedByAncestor.values()]; + if (matches.length === 1) { + el = _resolveInteractiveAncestor(matches[0].e) || matches[0].e; + textResolvedExact = (usedMode === 'exact'); + } + } if (!el && matches.length > 1) { // Prefer interactive elements over passive children (label, span, etc.) const interactiveMatches = matches.filter(m => _isInteractive(m.e)); if (interactiveMatches.length === 1) { matches = interactiveMatches; } else { + const hitTargetPool = (interactiveMatches.length > 1 ? interactiveMatches : matches).filter(m => { + try { + const r = m.e.getBoundingClientRect(); + if (r.width < 1 || r.height < 1) return false; + if (m.e.disabled === true || m.e.getAttribute?.('aria-disabled') === 'true') return false; + const hit = _shadowAwareElementFromPoint( + Math.round(r.left + r.width / 2), + Math.round(r.top + r.height / 2), + ); + return !!hit && (hit === m.e || m.e.contains?.(hit) || hit.contains?.(m.e)); + } catch { + return false; + } + }); + if (hitTargetPool.length === 1) { + el = _resolveInteractiveAncestor(hitTargetPool[0].e) || hitTargetPool[0].e; + textResolvedExact = (usedMode === 'exact'); + matches = hitTargetPool; + } else if (hitTargetPool.length > 1 && hitTargetPool.length < matches.length) { + matches = hitTargetPool; + } + if (matches.length > 1) { // Build rich candidates: position (rect), tag, role, surrounding // context (closest landmark/dialog/button text), and precomputed // click centers. When the same text appears twice, the model needs @@ -1899,6 +1990,7 @@ error: `Ambiguous text match for "${params.text}" (mode=${usedMode}, matches=${matches.length})${_scopeNote}. ${candidates.length} candidates returned with cx/cy (precomputed click center, in CSS pixels) and ancestor context. Pick one and call click({x: candidate.cx, y: candidate.cy, coordinate_space: "css"}) — no arithmetic needed. Use the ancestor field to disambiguate (e.g. an alertdialog's Cancel vs a form's Cancel sit in different containers). Do NOT retry click({text: "${params.text}"}) — it will fail the same way.`, candidates, }; + } } } if (!el) { @@ -4146,6 +4238,38 @@ fieldMeta: _fieldMeta(el), rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height } }; } + const _COMPOSER_UTILITY_LABEL_RE = new RegExp( + '^(?:save\\s*(?:&|and)?\\s*(?:close|draft|draft\\s*&\\s*close)|save\\s*&\\s*close' + + '|minimi[sz]e|exit\\s+full\\s+screen|full\\s+screen|pop-?out|expand|collapse' + + '|discard(?:\\s+draft)?|delete\\s+draft|print|check\\s+spelling|plain\\s+text(?:\\s+mode)?' + + '|attach(?:\\s+(?:files?|photos?|images?|documents?))?|insert\\s+(?:link|photo|image|file|drive|emoji|signature|contact|table|drawing|note)' + + '|emoji|emoticon|bold|italic|underline|strikethrough|align|numbered\\s+list|bulleted\\s+list' + + '|indent|outdent|undo|redo|more\\s+options|formatting(?:\\s+options)?|text\\s+formatting' + + '|remove\\s+formatting|font|text\\s+color|highlight|edit\\s+subject|confidential\\s+mode)', + 'i', + ); + + function _isComposerUtilityControl(el) { + if (!el) return false; + try { + const label = String( + el.getAttribute?.('aria-label') + || el.getAttribute?.('title') + || el.getAttribute?.('data-tooltip') + || el.value + || el.innerText + || el.textContent + || '', + ).replace(/\s+/g, ' ').trim(); + if (label && _COMPOSER_UTILITY_LABEL_RE.test(label)) return true; + const formattingScope = el.closest?.('[role="toolbar"],[aria-label*="formatting" i],[aria-label*="format" i]'); + if (formattingScope && formattingScope !== el) return true; + return !!el.hasAttribute?.('aria-pressed'); + } catch { + return false; + } + } + function _probeMessageRecipientGuard(params = {}) { try { const tool = String(params.tool || ''); @@ -4342,7 +4466,11 @@ } else if (tool === 'click') { // Match dispatch precedence, modal scope, candidate text and match // mode. A supplied selector must not approve a different text click. - if (typeof args.text === 'string' && args.text) { + if (typeof args.ref_id === 'string' && args.ref_id + && typeof window.__wb_ax_lookup === 'function') { + target = window.__wb_ax_lookup(args.ref_id); + } + if (!target && typeof args.text === 'string' && args.text) { const needle = args.text.toLowerCase(); const scope = _findTopmostModal() || document; const candidates = _clickTextCandidates(scope); @@ -4360,11 +4488,11 @@ if (matches.length === 1) target = _resolveInteractiveAncestor(matches[0].e); if (matches.length) break; } - } else if (typeof args.selector === 'string' && args.selector) { + } else if (!target && typeof args.selector === 'string' && args.selector) { target = safeIndexedQuerySelector(args.selector, args.matchIndex).element; - } else if (Number.isInteger(args.index) && args.index >= 0) { + } else if (!target && Number.isInteger(args.index) && args.index >= 0) { target = queryInteractiveForToolIndex()[args.index] || null; - } else if (Number.isFinite(args.x) && Number.isFinite(args.y)) { + } else if (!target && Number.isFinite(args.x) && Number.isFinite(args.y)) { target = _shadowAwareElementFromPoint(args.x, args.y); } targetResolved = !!target; @@ -4651,9 +4779,19 @@ return { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; } if (active !== layoutComposer) { - return verifiedNavigationEditable(active) - ? { success: true, messageSend: false, conclusive: true, identityCandidates: [] } - : { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; + if (verifiedNavigationEditable(active)) { + return { success: true, messageSend: false, conclusive: true, identityCandidates: [] }; + } + try { + const composeScopeOf = (el) => el?.closest?.( + 'form, [role="dialog"], [role="form"], [aria-label*="compose" i], [class*="compose" i]', + ); + const activeScope = composeScopeOf(active); + if (activeScope && activeScope === composeScopeOf(layoutComposer)) { + return { success: true, messageSend: false, conclusive: true, identityCandidates: [] }; + } + } catch { /* fall through to the conservative result */ } + return { success: true, messageSend: null, conclusive: false, identityCandidates: [] }; } composer = layoutComposer; dispatchTarget = active; @@ -4706,6 +4844,16 @@ identityCandidates: [], }; } + if (_isComposerUtilityControl(control)) { + return { + success: true, + messageSend: false, + conclusive: true, + composerUtility: true, + reasonCode: 'non_messaging_target', + identityCandidates: [], + }; + } composer = layoutComposer; if (!composer) { const actionLabel = compact( @@ -4719,11 +4867,30 @@ ); const composerSetup = params.adapterName === 'gmail' && /^(?:reply|reply all|forward)$/i.test(actionLabel); + const messageCommit = /(?:^|[^\p{L}])(?:send|enviar|envoyer|invia|senden|verzenden|gönder|отправить|送信|发送|發送|보내|إرسال|ارسال)(?:[^\p{L}]|$)/iu + .test(actionLabel); + const messagingSurface = (() => { + try { + return !!control.closest?.('[class*="msg-"],[class*="messaging"],[id*="messaging"],[data-messaging],[data-test-messaging],[role="log"]'); + } catch { return false; } + })(); + if (!composerSetup && !messageCommit && !messagingSurface) { + return { + success: true, + messageSend: false, + conclusive: true, + composerAvailable: false, + nonMessagingTarget: true, + reasonCode: 'non_messaging_target', + identityCandidates: [], + }; + } return { success: true, messageSend: null, conclusive: false, composerAvailable: false, + reasonCode: messageCommit ? 'message_commit_without_composer' : 'no_composer_surface', ...(composerSetup ? { composerSetup: true } : {}), identityCandidates: [], }; @@ -4916,6 +5083,47 @@ })(); gmailComposeRoot = composeRoot; const recipients = collectGmailRecipients(composeRoot); + // A typed, uncommitted Gmail address only exists in the visible + // recipient input. Include it so confirmation remains bound to what + // is actually on screen before the UI turns it into a chip. + const collectRecipientInputValues = (root) => { + if (!root?.querySelectorAll) return; + let inputs = []; + try { + inputs = Array.from(root.querySelectorAll( + 'input, textarea, [contenteditable="true"], [contenteditable=""]', + )).slice(0, 40); + } catch { + return; + } + for (const input of inputs) { + if (!visible(input) || input === composer || composer?.contains?.(input)) continue; + const role = recipientRole(input, root); + if (!role) continue; + const raw = String('value' in input ? (input.value || '') : (input.innerText || input.textContent || '')); + if (!raw || raw.length > 2000) continue; + for (const piece of raw.split(/[,;\n]+/)) { + const candidate = compact(piece, 240); + const match = candidate.match(/[^\s<>@,;]+@[^\s<>@,;.]+\.[^\s<>@,;]+/); + if (!match) continue; + const email = match[0]; + const key = normalizedIdentity(email); + if (!key) continue; + const recipientKey = `${role}:${key}`; + const prior = recipients.get(recipientKey) || { identity: email, role, aliases: new Map() }; + prior.aliases.set(key, email); + const display = compact(candidate.replace(/<[^>]*>/g, ''), 240); + if (display && display !== email) { + const normalizedDisplay = normalizedIdentity(display); + if (normalizedDisplay && !prior.aliases.has(normalizedDisplay)) { + prior.aliases.set(normalizedDisplay, display); + } + } + recipients.set(recipientKey, prior); + } + } + }; + collectRecipientInputValues(composeRoot); for (const [recipientKey, recipient] of recipients) { const emailKey = recipientKey.slice(recipientKey.indexOf(':') + 1); const identity = recipient.aliases.get(emailKey) || [...recipient.aliases.values()][0] || recipient.identity || ''; @@ -5033,6 +5241,94 @@ } } + // Generic mail surfaces expose recipient fields and chips without a + // provider-specific adapter. Read only explicit recipient semantics; + // an ordinary contact form's email field must not become a message + // recipient, and a generic chat still requires one header identity. + if (observedRecipientCandidates.length === 0) { + const genericMailRecipientMode = params.adapterName === 'generic-messaging' + && params.supportsRecipientSets === true; + const RECIPIENT_FIELD_RE = new RegExp( + '(?:^|[^\\p{L}])(?:to|to\\s+recipients?|recipients?|send\\s+to|email|e-?mail(?:\\s+address)?' + + '|destinatario|destinataria|destinataire|para|aan|kime|do|til|komu|\\u0644\\u0625\\u0649' + + '|\\u6536\\u4ef6\\u4eba|\\u5b9b\\u5148|\\ubc1b\\ub294\\s*\\uc0ac\\ub78c)(?:[^\\p{L}]|$)', + 'iu', + ); + const fieldLooksLikeRecipient = (el) => { + try { + if (/^email$/i.test(String(el.getAttribute?.('type') || ''))) return genericMailRecipientMode; + if (/^email$/i.test(String(el.getAttribute?.('autocomplete') || ''))) return genericMailRecipientMode; + const haystack = [ + el.getAttribute?.('name'), + el.getAttribute?.('aria-label'), + el.getAttribute?.('placeholder'), + el.getAttribute?.('id'), + el.getAttribute?.('data-testid'), + el.getAttribute?.('autocomplete'), + el.labels ? Array.from(el.labels).map(label => label.innerText).join(' ') : '', + ].filter(Boolean).join(' '); + return !!haystack && RECIPIENT_FIELD_RE.test(haystack); + } catch { return false; } + }; + const pushRecipient = (identity, extraAliases = []) => { + const raw = compact(identity, 240); + const normalized = raw ? normalizedIdentity(raw) : ''; + if (!normalized) return; + const existing = observedRecipientCandidates.find( + item => normalizedIdentity(item.identity) === normalized, + ); + const aliases = Array.from(new Set([ + raw, ...extraAliases, ...(existing?.aliases || []), + ].filter(Boolean))); + const entry = { identity: raw, role: 'to', aliases }; + if (existing) Object.assign(existing, entry); + else observedRecipientCandidates.push(entry); + if (!strongRecipients.some(item => normalizedIdentity(item.identity) === normalized)) { + strongRecipients.push({ identity: raw, role: 'to' }); + } + }; + const extractAddresses = (rawValue) => { + const raw = String(rawValue || ''); + if (!raw || raw.length > 2000) return; + for (const piece of raw.split(/[,;\n]+/)) { + const candidate = compact(piece, 240); + const match = candidate.match(/[^\s<>@,;]+@[^\s<>@,;.]+\.[^\s<>@,;]+/); + if (!match) continue; + const display = compact(candidate.replace(/<[^>]*>/g, ''), 240); + pushRecipient(match[0], display && display !== match[0] ? [display] : []); + } + }; + try { + for (const el of Array.from(document.querySelectorAll( + 'input, textarea, [contenteditable="true"], [contenteditable=""]', + )).slice(0, 60)) { + if (!visible(el) || el === composer || composer?.contains?.(el)) continue; + if (!fieldLooksLikeRecipient(el)) continue; + extractAddresses('value' in el ? el.value : (el.innerText || el.textContent || '')); + } + } catch { /* structural read is best-effort */ } + try { + const chipScope = composer.closest?.('[role="dialog"],form,[role="main"]') || document; + for (const el of Array.from(chipScope.querySelectorAll( + '[email],[data-email],[data-hovercard-id],a[href^="mailto:" i],input[type="email"]', + )).slice(0, 40)) { + if (!visible(el) || el === composer || composer?.contains?.(el)) continue; + if (!genericMailRecipientMode + && /^email$/i.test(String(el.getAttribute?.('type') || ''))) continue; + const mailto = String(el.getAttribute?.('href') || '') + .replace(/^mailto:/i, '').split('?')[0]; + const address = [ + el.getAttribute?.('email'), el.getAttribute?.('data-email'), + el.getAttribute?.('data-hovercard-id'), mailto, + 'value' in el ? el.value : '', + ].map(value => compact(value, 240)).find(value => String(value || '').includes('@')) || ''; + if (!address) continue; + const display = compact(el.innerText || el.textContent, 240); + pushRecipient(address, display && display !== address ? [display] : []); + } + } catch { /* structural read is best-effort */ } + } + const composerText = (() => { try { if ('value' in composer) return String(composer.value || ''); @@ -5142,6 +5438,7 @@ // --- Message handler --- browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.target !== 'content') return; + if (WEBBRAIN_GENERATION !== (Number(window.__webbrain_generation) || 0)) return; const actionDeadlineAt = Number(msg.actionDeadlineAt) || 0; const actionDeadlineExpired = () => actionDeadlineAt > 0 && Date.now() >= actionDeadlineAt; if (actionDeadlineExpired()) { @@ -6760,7 +7057,16 @@ return value; }; - const result = handler(); + let result; + try { + result = handler(); + } catch (err) { + sendResponse(withLiveDocumentScope({ + success: false, + error: `${msg.action} failed: ${err?.message || String(err)}`, + })); + return; + } if (result instanceof Promise) { // Always settle sendResponse — a rejecting handler (e.g. a throwing // DOM API) must not leave the caller's await hanging forever. @@ -6773,6 +7079,14 @@ ); return true; // async } + if (result === undefined || result === null) { + sendResponse(withLiveDocumentScope({ + success: false, + noResult: true, + error: `${msg.action} returned no result (handler produced no payload).`, + })); + return; + } sendResponse(withLiveDocumentScope(result)); }); diff --git a/src/firefox/src/ui/sidepanel.js b/src/firefox/src/ui/sidepanel.js index 9c062636a..12c8ec3e6 100644 --- a/src/firefox/src/ui/sidepanel.js +++ b/src/firefox/src/ui/sidepanel.js @@ -1402,6 +1402,12 @@ function playCompletionSound() { } catch { /* ignore */ } } +// A clarification or permission card needs the same attention cue as a +// completed run. It shares the user's existing notification-sound preference. +function playClarifySound() { + playCompletionSound(); +} + function triggerCompletionConfetti() { if (!completionConfettiEnabled) return; if (globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) return; @@ -9590,6 +9596,7 @@ function workflowHealingTargetLabel(target) { function renderClarifyCard(data) { hideActivity(); + playClarifySound(); const tabId = data?.scheduledTabId ?? data?.tabId ?? currentTabId; if (tabId == null) return; const scheduledJobId = data?.scheduledJobId ? String(data.scheduledJobId) : ''; diff --git a/test/run.js b/test/run.js index 6ac77d07c..4faa4ce7b 100644 --- a/test/run.js +++ b/test/run.js @@ -4972,7 +4972,22 @@ test('matches Douyin video and live surfaces with verification and publication g assert.equal(getMessageRecipientGuardPolicy('https://www.douyin.com/chat/123')?.adapterName, 'douyin'); assert.equal(getMessageRecipientGuardPolicy('https://www.douyin.com/video/123'), null); assert.equal(getMessageRecipientGuardPolicy('https://creator.douyin.com/creator-micro/content/upload'), null); - assert.equal(getMessageRecipientGuardPolicy('https://example.com/chat'), null); + assert.deepEqual(getMessageRecipientGuardPolicy('https://example.com/chat'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, + }); + assert.deepEqual(getMessageRecipientGuardPolicyFx('https://example.com/chat'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, + }); + assert.deepEqual(getMessageRecipientGuardPolicy('https://mail.example.com/compose'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, supportsRecipientSets: true, + }); + assert.deepEqual(getMessageRecipientGuardPolicyFx('https://mail.example.com/compose'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, supportsRecipientSets: true, + }); + assert.equal(getMessageRecipientGuardPolicy('https://example.com/contact'), null); + assert.equal(getMessageRecipientGuardPolicyFx('https://example.com/contact'), null); + assert.equal(getMessageRecipientGuardPolicy('https://example.com/inbox'), null); + assert.equal(getMessageRecipientGuardPolicyFx('https://example.com/inbox'), null); }); test('direct-message recipient guard uses structured intent and exact active identity evidence', async () => { @@ -5478,12 +5493,36 @@ test('direct-message recipient guard uses structured intent and exact active ide assert.deepEqual(getPolicy('https://www.linkedin.com/messaging/thread/2-abc/'), { adapterName: 'linkedin', verifyActiveRecipient: true, }); - assert.equal(getPolicy('https://linkedin.example.com/messaging/'), null); + assert.deepEqual(getPolicy('https://linkedin.example.com/messaging/'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, + }); + assert.deepEqual(getPolicy('https://example.com/chat/rooms/42'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, + }); + assert.deepEqual(getPolicy('https://mail.example.com/compose'), { + adapterName: 'generic-messaging', verifyActiveRecipient: true, supportsRecipientSets: true, + }); + assert.equal(getPolicy('https://example.com/contact'), null); + assert.equal(getPolicy('https://example.com/inbox'), null); } for (const [label, AgentClass] of [['chrome', AgentCh], ['firefox', AgentFx]]) { const agent = new AgentClass({ getVisionProvider: async () => null }); const tabId = label === 'chrome' ? 27101 : 27102; + agent.autoScreenshot = 'state_change'; + assert.equal(agent.autoScreenshotBursts.size, 0, `${label}: screenshot predicate inherited stale burst state`); + assert.deepEqual( + [1, 2, 3, 4, 5].map(() => agent._advanceAutoScreenshotBurst(tabId, 'click')), + [true, false, false, false, true], + `${label}: repeated action screenshots were not throttled at the batch boundary`, + ); + assert.equal(agent._advanceAutoScreenshotBurst(tabId + 1, 'click'), true, + `${label}: screenshot throttle leaked across tabs`); + assert.equal(agent._shouldAutoScreenshot('click'), true, + `${label}: screenshot eligibility unexpectedly changed`); + assert.equal(agent.autoScreenshotBursts.get(tabId)?.streak, 5, + `${label}: screenshot predicate consumed a burst position`); + agent.autoScreenshotBursts.clear(); agent._currentUrl = async () => 'https://www.douyin.com/chat'; let probe = { success: true, @@ -5527,6 +5566,13 @@ test('direct-message recipient guard uses structured intent and exact active ide 'https://www.douyin.com/chat', ); assert.equal(ambiguousPin.ok, false, `${label}: ambiguous active conversation was authorized`); + const genericGroupPin = await agent._pinActiveConversationMessagingTarget( + tabId, + { target_kind: 'active_conversation', recipients: [] }, + 'https://example.com/chat/rooms/42', + ); + assert.equal(genericGroupPin.ok, false, + `${label}: generic group chat was accepted as a single authorized recipient`); probe = { success: false, @@ -7049,7 +7095,7 @@ test('direct-message recipient probe accepts only a unique active-thread header const distantControl = element('Forward', { left: 20, right: 140, top: 300, bottom: 350, width: 120, height: 50, }, { tagName: 'BUTTON', role: 'button' }); - distantControl.closest = () => distantControl; + distantControl.closest = (selector) => selector.includes('button') ? distantControl : null; const conversationRail = element('', { left: 0, right: 340, top: 80, bottom: 900, width: 340, height: 820, }, { role: 'list', clientHeight: 820, scrollHeight: 820 }); @@ -7128,7 +7174,10 @@ test('direct-message recipient probe accepts only a unique active-thread header const composedParentEnd = source.indexOf('\n\n function ', composedParentStart + 1); assert.ok(composedParentStart >= 0 && composedParentEnd > composedParentStart, `${prefix}: composed-parent helper must be available to visibility checks`); - const candidatesStart = source.indexOf(' function _clickTextCandidates('); + const clickCandidatesStart = source.indexOf(' function _clickTextCandidates('); + const candidatesStart = source.lastIndexOf(' const _domRevision', clickCandidatesStart) >= 0 + ? source.lastIndexOf(' const _domRevision', clickCandidatesStart) + : clickCandidatesStart; const candidatesEnd = source.indexOf('\n\n let _lastClickIdent', candidatesStart); Object.assign(context, { _siteInteractiveSelectors: () => [], @@ -7141,8 +7190,15 @@ test('direct-message recipient probe accepts only a unique active-thread header }); const visibilityStart = source.indexOf(' function _hasVisibleBox('); const visibilityEnd = source.indexOf('\n\n function ', visibilityStart + 1); + const recipientHelpersStart = [ + source.indexOf(' const _MESSAGE_COMMIT_LABEL_RE'), + source.indexOf(' const _COMPOSER_UTILITY_LABEL_RE'), + ].filter(index => index >= 0).sort((a, b) => a - b)[0] ?? -1; + const recipientHelpers = recipientHelpersStart >= 0 + ? source.slice(recipientHelpersStart, start) + : ''; const probe = vm.runInNewContext( - `${source.slice(composedParentStart, composedParentEnd)}; ${source.slice(visibilityStart, visibilityEnd)}; ${source.slice(candidatesStart, candidatesEnd)}; (${source.slice(start, end)})`, context, + `${source.slice(composedParentStart, composedParentEnd)}; ${source.slice(visibilityStart, visibilityEnd)}; ${source.slice(candidatesStart, candidatesEnd)}; ${recipientHelpers}; (${source.slice(start, end)})`, context, ); const observationResult = probe({ tool: 'observe_active_conversation', args: {} }); const enterResult = probe({ tool: 'press_keys', args: { key: 'Enter' } }); @@ -41868,7 +41924,7 @@ test('web hero social proof uses recognizable brand and users icons', () => { ); assert.match( fs.readFileSync(path.join(ROOT, 'web/assets/marktechpost.svg'), 'utf8'), - /]+width="32" height="32" viewBox="0 0 32 32"/, + /]*\bwidth="32")(?=[^>]*\bheight="32")(?=[^>]*\bviewBox="0 0 32 32")/, 'web assets: MarkTechPost favicon should preserve its square aspect ratio', ); }); @@ -86730,7 +86786,9 @@ test('browser batches keep leading reads, then require fresh evidence after unsa const messages = []; agent._ensureGateSetting = async () => {}; agent._skipPermissionGate = true; - agent._currentUrl = async () => 'https://mail.example.test/inbox'; + // This batching check is deliberately not a messaging surface: generic + // recipient guards correctly stop send-like controls on mail routes. + agent._currentUrl = async () => 'https://www.example.test/docs'; agent._rememberMastodonObservation = async () => null; agent._recordProgressObservation = async () => null; agent._autoRecordProgressAction = () => null;