mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(dashboard): UI polish — modals, layout, consistency, test fixes
Dashboard UX polish pass — consolidates create forms into modals triggered from the page header, fixes layout inconsistencies, adds scroll-to navigation for the Keys page, and aligns the TokenBar with the design system. Changes: - App.tsx: add padding to sidebar header - resolve-page-title.ts: add missing routes, better fallback title - en.ts: fix nav labels (Profiles was 'profiles : multi agents') - ModelsPage: two-col layout, auxiliary tasks modal, TokenBar redesign - ProfilesPage: create button in header, form in modal, Checkbox component - CronPage: create button in header, form in modal - EnvPage: scroll-to sub-nav in header, fix text overflow Modal and dialog standardization: - Replace all native confirm()/window.confirm() with ConfirmDialog (OAuthProvidersCard, PluginsPage, ModelsPage, ConfigPage) - Add useModalBehavior hook (Escape-to-close, scroll lock, focus restore) - Apply hook to ProfilesPage, CronPage, AuxiliaryTasksModal Component fixes (from PR review): - Checkbox: fix controlled/uncontrolled mismatch, add focus-visible ring - TokenBar: add rounded-full to legend dots, remove dead code CI/test fixes: - Fix TS unused imports (noUnusedLocals), type-narrow PickerTarget union - Add windows-footgun suppression on platform-guarded os.killpg - Fix 19 stale unit tests + 9 e2e tests broken by recent main changes - Restore minimal example-dashboard plugin for plugin auth test
This commit is contained in:
parent
dd0923bb89
commit
fc3fd6bb6b
27 changed files with 788 additions and 295 deletions
|
|
@ -660,6 +660,7 @@ class TestAuxiliaryPoolAwareness:
|
|||
with (
|
||||
patch("agent.auxiliary_client.load_pool", return_value=_Pool()),
|
||||
patch("agent.auxiliary_client.OpenAI") as mock_openai,
|
||||
patch("hermes_cli.models.get_nous_recommended_aux_model", return_value=None),
|
||||
):
|
||||
from agent.auxiliary_client import _try_nous
|
||||
|
||||
|
|
|
|||
|
|
@ -222,6 +222,9 @@ def make_runner(platform: Platform, session_entry: SessionEntry = None) -> "Gate
|
|||
runner._capture_gateway_honcho_if_configured = lambda *a, **kw: None
|
||||
runner._emit_gateway_run_progress = AsyncMock()
|
||||
|
||||
# Disable destructive slash confirm gate so /new executes immediately
|
||||
runner._read_user_config = lambda: {"approvals": {"destructive_slash_confirm": False}}
|
||||
|
||||
runner.pairing_store = MagicMock()
|
||||
runner.pairing_store._is_rate_limited = MagicMock(return_value=False)
|
||||
runner.pairing_store.generate_code = MagicMock(return_value="ABC123")
|
||||
|
|
|
|||
|
|
@ -176,8 +176,8 @@ class TestStreamingConfig:
|
|||
"fresh_final_after_seconds": "oops",
|
||||
}
|
||||
)
|
||||
assert restored.edit_interval == 1.0
|
||||
assert restored.buffer_threshold == 40
|
||||
assert restored.edit_interval == 0.8
|
||||
assert restored.buffer_threshold == 24
|
||||
assert restored.fresh_final_after_seconds == 60.0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ only renders as a voice bubble when explicitly flagged) and via
|
|||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -106,6 +106,16 @@ async def test_base_adapter_routes_voice_tagged_telegram_ogg_media_tag_to_voice_
|
|||
adapter.send_document.assert_not_awaited()
|
||||
|
||||
|
||||
def _fake_runner(thread_meta):
|
||||
"""Build a fake GatewayRunner-like object with the helper methods needed by
|
||||
_deliver_media_from_response."""
|
||||
runner = SimpleNamespace(
|
||||
_thread_metadata_for_source=lambda source, anchor=None: thread_meta,
|
||||
_reply_anchor_for_event=lambda event: None,
|
||||
)
|
||||
return runner
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_delivery_routes_telegram_flac_media_tag_to_document_sender():
|
||||
event = _event(thread_id="topic-1")
|
||||
|
|
@ -121,7 +131,7 @@ async def test_streaming_delivery_routes_telegram_flac_media_tag_to_document_sen
|
|||
)
|
||||
|
||||
await GatewayRunner._deliver_media_from_response(
|
||||
object(),
|
||||
_fake_runner({"thread_id": "topic-1"}),
|
||||
"MEDIA:/tmp/speech.flac",
|
||||
event,
|
||||
adapter,
|
||||
|
|
@ -150,7 +160,7 @@ async def test_streaming_delivery_routes_non_voice_telegram_ogg_media_tag_to_doc
|
|||
)
|
||||
|
||||
await GatewayRunner._deliver_media_from_response(
|
||||
object(),
|
||||
_fake_runner({"thread_id": "topic-1"}),
|
||||
"MEDIA:/tmp/speech.ogg",
|
||||
event,
|
||||
adapter,
|
||||
|
|
@ -181,7 +191,7 @@ async def test_streaming_delivery_routes_telegram_mp3_media_tag_to_voice_sender(
|
|||
)
|
||||
|
||||
await GatewayRunner._deliver_media_from_response(
|
||||
object(),
|
||||
_fake_runner({"thread_id": "topic-1"}),
|
||||
"MEDIA:/tmp/speech.mp3",
|
||||
event,
|
||||
adapter,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ def _make_runner(hermes_home=None):
|
|||
runner._pending_messages = {}
|
||||
runner._pending_approvals = {}
|
||||
runner._failed_platforms = {}
|
||||
# config is accessed by _check_slash_access and quick_commands lookup;
|
||||
# None makes policy_for_source return a disabled (allow-all) policy.
|
||||
runner.config = None
|
||||
# Bypass the destructive-slash confirm gate — this test exercises
|
||||
# update-prompt interception, not the confirm prompt.
|
||||
runner._read_user_config = lambda: {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class TestVerboseCommand:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_defaults_to_all_when_no_tool_progress_set(self, tmp_path, monkeypatch):
|
||||
"""When tool_progress is not in config, defaults to 'all' then cycles to verbose."""
|
||||
"""When tool_progress is not in config, defaults to platform default then cycles."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
|
|
@ -143,17 +143,17 @@ class TestVerboseCommand:
|
|||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
|
||||
# Telegram default is "all" (high tier) → cycles to verbose
|
||||
assert "VERBOSE" in result
|
||||
# Telegram platform default is "new" → cycles to "all"
|
||||
assert "ALL" in result
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "verbose"
|
||||
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "all"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_platform_isolation(self, tmp_path, monkeypatch):
|
||||
"""Cycling /verbose on Telegram doesn't change Slack's setting.
|
||||
|
||||
Without a global tool_progress, each platform uses its built-in
|
||||
default: Telegram = 'all' (high tier), Slack = 'off' (quiet Slack default).
|
||||
default: Telegram = 'new' (overridden high tier), Slack = 'off' (quiet Slack default).
|
||||
"""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
|
@ -178,8 +178,8 @@ class TestVerboseCommand:
|
|||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
platforms = saved["display"]["platforms"]
|
||||
# Telegram: all -> verbose (high tier default = all)
|
||||
assert platforms["telegram"]["tool_progress"] == "verbose"
|
||||
# Telegram: new -> all (platform default = new)
|
||||
assert platforms["telegram"]["tool_progress"] == "all"
|
||||
# Slack: off -> new (first /verbose cycle from quiet default)
|
||||
assert platforms["slack"]["tool_progress"] == "new"
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
def test_profiles_nav_label_uses_short_multi_agents_copy():
|
||||
def test_profiles_nav_label_uses_short_copy():
|
||||
en_i18n = Path(__file__).resolve().parents[2] / "web" / "src" / "i18n" / "en.ts"
|
||||
|
||||
content = en_i18n.read_text(encoding="utf-8")
|
||||
|
||||
assert 'profiles: "profiles : multi agents"' in content
|
||||
assert "Profiles: Running Multiple Agents" not in content
|
||||
# Nav label should be the clean short form, not the old verbose string
|
||||
assert 'profiles: "Profiles"' in content
|
||||
assert "profiles : multi agents" not in content
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ rather than leaving zombie processes or telling users to manually restart
|
|||
when launchd will auto-respawn.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
|
@ -1068,13 +1069,18 @@ class TestFindGatewayPidsExclude:
|
|||
|
||||
def test_excludes_specified_pids(self, monkeypatch):
|
||||
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
|
||||
# Bypass /proc scan so the subprocess (ps) fallback is used
|
||||
_real_isdir = os.path.isdir
|
||||
monkeypatch.setattr("os.path.isdir", lambda p: False if p == "/proc" else _real_isdir(p))
|
||||
monkeypatch.setattr(gateway_cli, "_get_service_pids", lambda: set())
|
||||
monkeypatch.setattr(gateway_cli, "_get_ancestor_pids", lambda: {999})
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 0,
|
||||
stdout=(
|
||||
"user 100 0.0 0.0 0 0 ? S 00:00 0:00 python gateway/run.py\n"
|
||||
"user 200 0.0 0.0 0 0 ? S 00:00 0:00 python gateway/run.py\n"
|
||||
"100 python gateway/run.py\n"
|
||||
"200 python gateway/run.py\n"
|
||||
),
|
||||
stderr="",
|
||||
)
|
||||
|
|
@ -1082,19 +1088,24 @@ class TestFindGatewayPidsExclude:
|
|||
monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr("os.getpid", lambda: 999)
|
||||
|
||||
pids = gateway_cli.find_gateway_pids(exclude_pids={100})
|
||||
pids = gateway_cli.find_gateway_pids(exclude_pids={100}, all_profiles=True)
|
||||
assert 100 not in pids
|
||||
assert 200 in pids
|
||||
|
||||
def test_no_exclude_returns_all(self, monkeypatch):
|
||||
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
|
||||
# Bypass /proc scan so the subprocess (ps) fallback is used
|
||||
_real_isdir = os.path.isdir
|
||||
monkeypatch.setattr("os.path.isdir", lambda p: False if p == "/proc" else _real_isdir(p))
|
||||
monkeypatch.setattr(gateway_cli, "_get_service_pids", lambda: set())
|
||||
monkeypatch.setattr(gateway_cli, "_get_ancestor_pids", lambda: {999})
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 0,
|
||||
stdout=(
|
||||
"user 100 0.0 0.0 0 0 ? S 00:00 0:00 python gateway/run.py\n"
|
||||
"user 200 0.0 0.0 0 0 ? S 00:00 0:00 python gateway/run.py\n"
|
||||
"100 python gateway/run.py\n"
|
||||
"200 python gateway/run.py\n"
|
||||
),
|
||||
stderr="",
|
||||
)
|
||||
|
|
@ -1102,7 +1113,7 @@ class TestFindGatewayPidsExclude:
|
|||
monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run)
|
||||
monkeypatch.setattr("os.getpid", lambda: 999)
|
||||
|
||||
pids = gateway_cli.find_gateway_pids()
|
||||
pids = gateway_cli.find_gateway_pids(all_profiles=True)
|
||||
assert 100 in pids
|
||||
assert 200 in pids
|
||||
|
||||
|
|
@ -1111,6 +1122,10 @@ class TestFindGatewayPidsExclude:
|
|||
profile_dir.mkdir(parents=True)
|
||||
monkeypatch.setattr(gateway_cli, "is_windows", lambda: False)
|
||||
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: profile_dir)
|
||||
# Bypass /proc scan so the subprocess (ps) fallback is used
|
||||
_real_isdir = os.path.isdir
|
||||
monkeypatch.setattr("os.path.isdir", lambda p: False if p == "/proc" else _real_isdir(p))
|
||||
monkeypatch.setattr(gateway_cli, "_get_ancestor_pids", lambda: {999})
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ class TestClientCacheBoundedGrowth:
|
|||
_get_cached_client,
|
||||
)
|
||||
|
||||
key = ("test_replace", True, "", "", "", (), False)
|
||||
key = ("test_replace", True, "", "", "", (), False, "")
|
||||
|
||||
# Simulate a stale entry from a closed loop
|
||||
old_loop = asyncio.new_event_loop()
|
||||
|
|
|
|||
|
|
@ -945,7 +945,8 @@ class TestAuxiliaryClientProviderPriority:
|
|||
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
||||
from agent.auxiliary_client import get_text_auxiliary_client
|
||||
with patch("agent.auxiliary_client._read_nous_auth", return_value={"access_token": "nous-tok"}), \
|
||||
patch("agent.auxiliary_client.OpenAI") as mock:
|
||||
patch("agent.auxiliary_client.OpenAI") as mock, \
|
||||
patch("hermes_cli.models.get_nous_recommended_aux_model", return_value=None):
|
||||
client, model = get_text_auxiliary_client()
|
||||
assert model == "google/gemini-3-flash-preview"
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ class TestEphemeralMaxOutputTokens:
|
|||
agent.reasoning_config = None
|
||||
agent._is_anthropic_oauth = False
|
||||
agent._ephemeral_max_output_tokens = None
|
||||
agent._use_long_lived_prefix_cache = False
|
||||
|
||||
compressor = MagicMock()
|
||||
compressor.context_length = 200_000
|
||||
|
|
|
|||
|
|
@ -157,8 +157,14 @@ class TestHandleVisionAnalyzeFastPath:
|
|||
from agent.auxiliary_client import set_runtime_main, clear_runtime_main
|
||||
set_runtime_main("openrouter", "anthropic/claude-opus-4.6")
|
||||
try:
|
||||
coro = _handle_vision_analyze({"image_url": str(img), "question": "?"})
|
||||
result = asyncio.get_event_loop().run_until_complete(coro)
|
||||
# Mock decide_image_input_mode to always return "native" so the
|
||||
# fast path fires regardless of model-catalog state in CI.
|
||||
with patch(
|
||||
"agent.image_routing.decide_image_input_mode",
|
||||
return_value="native",
|
||||
):
|
||||
coro = _handle_vision_analyze({"image_url": str(img), "question": "?"})
|
||||
result = asyncio.get_event_loop().run_until_complete(coro)
|
||||
finally:
|
||||
clear_runtime_main()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue