From ca605174cf7dfae52c56826cf7951700f6143ef4 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 19:31:55 -0500 Subject: [PATCH 1/4] fix(desktop): zero Tip skipDelayDuration so every tip waits its delay --- apps/desktop/src/components/ui/tooltip.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/desktop/src/components/ui/tooltip.tsx b/apps/desktop/src/components/ui/tooltip.tsx index c482cefdc1cc..68e69d2aff7c 100644 --- a/apps/desktop/src/components/ui/tooltip.tsx +++ b/apps/desktop/src/components/ui/tooltip.tsx @@ -16,6 +16,11 @@ const HasTooltipProvider = React.createContext(false) function TooltipProvider({ delayDuration = 0, + // Radix's "skip" grace: after one tip opens, every trigger touched within + // this window opens INSTANTLY, delay bypassed. Its 300ms default meant a + // cursor sweeping the chrome still flashed a trail of tips despite the + // hover delay. Zero it so each tip independently honors `delayDuration`. + skipDelayDuration = 0, // Tips are labels, not interactive surfaces. Hoverable content + Radix's // pointer-grace bridge is what leaves tips stuck open — especially over // Electron `-webkit-app-region: drag` chrome where pointermove never fires @@ -28,6 +33,7 @@ function TooltipProvider({ data-slot="tooltip-provider" delayDuration={delayDuration} disableHoverableContent={disableHoverableContent} + skipDelayDuration={skipDelayDuration} {...props} /> ) From d89512107e7f562a380c2867870d0dcca67bae98 Mon Sep 17 00:00:00 2001 From: "hermes-seaeye[bot]" <307254004+hermes-seaeye[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:46:46 +0000 Subject: [PATCH 2/4] fmt(js): `npm run fix` on merge (#73004) Co-authored-by: github-actions[bot] --- .../thread/user-message-edit-gesture.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/assistant-ui/thread/user-message-edit-gesture.test.tsx b/apps/desktop/src/components/assistant-ui/thread/user-message-edit-gesture.test.tsx index 26a0a76681c5..b6deaa742d94 100644 --- a/apps/desktop/src/components/assistant-ui/thread/user-message-edit-gesture.test.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/user-message-edit-gesture.test.tsx @@ -1,4 +1,9 @@ -import { type AppendMessage, AssistantRuntimeProvider, ExportedMessageRepository, type ThreadMessage } from '@assistant-ui/react' +import { + type AppendMessage, + AssistantRuntimeProvider, + ExportedMessageRepository, + type ThreadMessage +} from '@assistant-ui/react' import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -105,6 +110,7 @@ function Harness({ onEdit }: { onEdit: (message: AppendMessage) => Promise describe('edit send arrow — macOS click gesture (blur races cancel)', () => { it('sends without an uncaught "Composer is not available" when the arrow-click blurs the editor', async () => { const uncaught: unknown[] = [] + const onUncaught = (err: unknown) => { uncaught.push(err) } From 5cffc53194062769f9a4018365cc42c6ab0cc685 Mon Sep 17 00:00:00 2001 From: Jaaneek Date: Mon, 27 Jul 2026 17:57:21 +0000 Subject: [PATCH 3/4] fix(xai): send Hermes-Agent User-Agent on chat/completions Direct tool HTTP calls already identified as Hermes-Agent, but the main OpenAI-SDK chat path still sent OpenAI/Python. Set Hermes-Agent/ for api.x.ai clients (xai + xai-oauth) so normal text traffic is attributed correctly. --- agent/agent_init.py | 4 ++ agent/auxiliary_client.py | 16 +++++++- plugins/model-providers/xai/__init__.py | 6 +++ run_agent.py | 5 +++ .../test_provider_attribution_headers.py | 40 +++++++++++++++++++ tools/xai_http.py | 10 +++++ 6 files changed, 80 insertions(+), 1 deletion(-) diff --git a/agent/agent_init.py b/agent/agent_init.py index 22f617b7f1c6..a99a0de89916 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -1156,6 +1156,10 @@ def init_agent( elif base_url_host_matches(effective_base, "chatgpt.com"): from agent.auxiliary_client import _codex_cloudflare_headers client_kwargs["default_headers"] = _codex_cloudflare_headers(api_key) + elif base_url_host_matches(effective_base, "x.ai"): + from tools.xai_http import hermes_xai_default_headers + + client_kwargs["default_headers"] = hermes_xai_default_headers() elif "default_headers" not in client_kwargs: # Fall back to profile.default_headers for providers that # declare custom headers (e.g. Kimi User-Agent on non-kimi.com diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 46b40b2a58a0..fa5bc6b80d7e 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -2794,7 +2794,13 @@ def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str return None, None api_key, base_url = resolved logger.debug("Auxiliary client: xAI OAuth (%s via Responses API)", model) - real_client = _create_openai_client(api_key=api_key, base_url=base_url) + from tools.xai_http import hermes_xai_default_headers + + real_client = _create_openai_client( + api_key=api_key, + base_url=base_url, + default_headers=hermes_xai_default_headers(), + ) return CodexAuxiliaryClient(real_client, model), model @@ -4892,6 +4898,10 @@ def _to_async_client(sync_client, model: str, is_vision: bool = False): async_kwargs["default_headers"] = {"User-Agent": "claude-code/0.1.0"} elif base_url_host_matches(sync_base_url, "integrate.api.nvidia.com"): async_kwargs["default_headers"] = build_nvidia_nim_headers(sync_base_url) + elif base_url_host_matches(sync_base_url, "x.ai"): + from tools.xai_http import hermes_xai_default_headers + + async_kwargs["default_headers"] = hermes_xai_default_headers() else: # Fall back to profile.default_headers for providers that declare # client-level headers on their ProviderProfile (e.g. attribution @@ -5515,6 +5525,10 @@ def resolve_provider_client( )) elif base_url_host_matches(base_url, "integrate.api.nvidia.com"): headers.update(build_nvidia_nim_headers(base_url)) + elif base_url_host_matches(base_url, "x.ai"): + from tools.xai_http import hermes_xai_default_headers + + headers.update(hermes_xai_default_headers()) else: # Fall back to profile.default_headers for providers that declare # client-level attribution headers on their profile (e.g. GMI diff --git a/plugins/model-providers/xai/__init__.py b/plugins/model-providers/xai/__init__.py index 8d73ae0199e8..273890dde95b 100644 --- a/plugins/model-providers/xai/__init__.py +++ b/plugins/model-providers/xai/__init__.py @@ -1,5 +1,6 @@ """xAI (Grok) provider profile.""" +from hermes_cli import __version__ as _HERMES_VERSION from providers import register_provider from providers.base import ProviderProfile @@ -10,6 +11,11 @@ xai = ProviderProfile( env_vars=("XAI_API_KEY",), base_url="https://api.x.ai/v1", auth_type="api_key", + # Attribution so xAI can identify Hermes chat/completions traffic. + # Must match tools.xai_http.hermes_xai_user_agent() (Hermes-Agent/). + # Host-based api.x.ai matching in run_agent / auxiliary_client also + # covers provider=xai-oauth. + default_headers={"User-Agent": f"Hermes-Agent/{_HERMES_VERSION}"}, ) register_provider(xai) diff --git a/run_agent.py b/run_agent.py index 71176691839e..f4d501914fe5 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5087,6 +5087,11 @@ class AIAgent: self._client_kwargs["default_headers"] = _codex_cloudflare_headers( self._client_kwargs.get("api_key", "") ) + elif base_url_host_matches(base_url, "x.ai"): + # Cover both provider=xai and provider=xai-oauth (api.x.ai). + from tools.xai_http import hermes_xai_default_headers + + self._client_kwargs["default_headers"] = hermes_xai_default_headers() else: # No URL-specific headers — check profile.default_headers before clearing. _ph_headers = None diff --git a/tests/run_agent/test_provider_attribution_headers.py b/tests/run_agent/test_provider_attribution_headers.py index dec932e34e0f..109f8bd4e9c3 100644 --- a/tests/run_agent/test_provider_attribution_headers.py +++ b/tests/run_agent/test_provider_attribution_headers.py @@ -159,6 +159,46 @@ def test_gmi_base_url_picks_up_profile_user_agent(mock_openai): assert headers["User-Agent"].startswith("HermesAgent/") +@patch("run_agent.OpenAI") +def test_xai_base_url_applies_hermes_user_agent(mock_openai): + """Direct xAI chat must send Hermes-Agent/* instead of OpenAI/Python.""" + mock_openai.return_value = MagicMock() + agent = AIAgent( + api_key="test-key", + base_url="https://api.x.ai/v1", + model="grok-4", + provider="xai", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + agent._apply_client_headers_for_base_url("https://api.x.ai/v1") + + headers = agent._client_kwargs["default_headers"] + assert headers["User-Agent"].startswith("Hermes-Agent/") + + +@patch("run_agent.OpenAI") +def test_xai_oauth_base_url_applies_hermes_user_agent(mock_openai): + """xai-oauth uses the same api.x.ai host and must get the Hermes UA.""" + mock_openai.return_value = MagicMock() + agent = AIAgent( + api_key="oauth-token", + base_url="https://api.x.ai/v1", + model="grok-4", + provider="xai-oauth", + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + agent._apply_client_headers_for_base_url("https://api.x.ai/v1") + + headers = agent._client_kwargs["default_headers"] + assert headers["User-Agent"].startswith("Hermes-Agent/") + + @patch("run_agent.OpenAI") def test_unknown_base_url_clears_default_headers(mock_openai): mock_openai.return_value = MagicMock() diff --git a/tools/xai_http.py b/tools/xai_http.py index 6d5287e4d868..81f2e264ca0c 100644 --- a/tools/xai_http.py +++ b/tools/xai_http.py @@ -97,6 +97,16 @@ def hermes_xai_user_agent() -> str: return f"Hermes-Agent/{__version__}" +def hermes_xai_default_headers() -> Dict[str, str]: + """Default headers for OpenAI-SDK and raw HTTP clients talking to xAI. + + Replaces the OpenAI Python SDK's identifying ``User-Agent: OpenAI/Python …`` + so chat/completions and Responses traffic is attributed as Hermes Agent, + matching the direct HTTP integrations (search, TTS, STT, image, video). + """ + return {"User-Agent": hermes_xai_user_agent()} + + def _load_config_section(section_name: str) -> Dict[str, Any]: """Return a top-level Hermes config section as a dict, or empty.""" try: From d83e858507a9bdb7f96c7a163d89c34c60909dcf Mon Sep 17 00:00:00 2001 From: Jaaneek Date: Mon, 27 Jul 2026 18:00:47 +0000 Subject: [PATCH 4/4] chore(xai): drop noisy default_headers comment --- plugins/model-providers/xai/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/model-providers/xai/__init__.py b/plugins/model-providers/xai/__init__.py index 273890dde95b..89b77d1e1a3a 100644 --- a/plugins/model-providers/xai/__init__.py +++ b/plugins/model-providers/xai/__init__.py @@ -11,10 +11,6 @@ xai = ProviderProfile( env_vars=("XAI_API_KEY",), base_url="https://api.x.ai/v1", auth_type="api_key", - # Attribution so xAI can identify Hermes chat/completions traffic. - # Must match tools.xai_http.hermes_xai_user_agent() (Hermes-Agent/). - # Host-based api.x.ai matching in run_agent / auxiliary_client also - # covers provider=xai-oauth. default_headers={"User-Agent": f"Hermes-Agent/{_HERMES_VERSION}"}, )