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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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/chrome/src/agent/adapters.js b/src/chrome/src/agent/adapters.js
index a59b82848..d2c9c6a68 100644
--- a/src/chrome/src/agent/adapters.js
+++ b/src/chrome/src/agent/adapters.js
@@ -18187,6 +18187,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.
@@ -18200,7 +18214,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/chrome/src/agent/agent.js b/src/chrome/src/agent/agent.js
index cbc65e4f0..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)
@@ -9101,14 +9104,49 @@ 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;
+ }
+ 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;
}
/**
@@ -11558,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);
@@ -13532,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;
}
@@ -13582,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;
}
}
@@ -13592,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));
@@ -23260,12 +23305,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 +23352,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;
@@ -23334,22 +23409,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 +23547,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;
}
@@ -25814,6 +25934,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
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);
@@ -39547,6 +39668,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) {
@@ -41539,6 +41702,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
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);
@@ -42946,6 +43110,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
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 1a2b3d951..82d28f8ff 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,54 @@
// 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, 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,
+ // 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',
+ ],
+ });
+ _domRevision.observedRoots.add(root);
+ } catch {
+ // 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();
+ 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
@@ -1379,17 +1440,30 @@
// 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('*')) {
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 = [];
+ for (const e of candidates) {
+ if (!_hasVisibleBox(e)) continue;
+ const text = _siteInteractionText(e).toLowerCase();
+ 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
+ // 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 +1537,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 +1628,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 +1737,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 +5110,78 @@
}
}
+ // 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.
+ 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 {
+ 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 +5382,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 +5408,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 +5699,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 +5771,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 +5801,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,15 +5847,21 @@
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) {
return { success: true, messageSend: null, conclusive: false, identityCandidates: [] };
@@ -5744,6 +6029,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 +6195,104 @@
}
}
+ // ── 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 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);
+ 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;
+ 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 { /* chip read is best-effort */ }
+ }
+
const composerText = (() => {
try {
if ('value' in composer) return String(composer.value || '');
@@ -5970,6 +6402,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 +8105,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 +8130,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));
});
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) : '';
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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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/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'),
- /