diff --git a/packages/claude-code-plugin/hooks/lib/hud_helpers.py b/packages/claude-code-plugin/hooks/lib/hud_helpers.py new file mode 100644 index 00000000..556c2bbd --- /dev/null +++ b/packages/claude-code-plugin/hooks/lib/hud_helpers.py @@ -0,0 +1,261 @@ +"""HUD state helper functions for consistent updates across hooks (#1324). + +Provides high-level helpers that multiple hooks can call to update the +HUD state file with standard field semantics. All functions silently +no-op on any error so they never block Claude Code. + +Field ownership: + SessionStart -> init_baseline() + UserPromptSubmit -> on_mode_entry() + PreToolUse -> on_tool_start() + PostToolUse -> on_tool_end() + Stop -> on_session_stop() +""" + +from __future__ import annotations + +import os +from typing import Optional + +from hud_state import update_hud_state + +# Map mode -> initial phase value +_MODE_PHASE_MAP = { + "PLAN": "planning", + "ACT": "executing", + "EVAL": "evaluating", + "AUTO": "cycling", +} + + +def on_mode_entry( + mode: str, + *, + state_file: Optional[str] = None, +) -> None: + """Reset workflow fields when a new mode is entered. + + Called from UserPromptSubmit after mode keyword detection. + + Args: + mode: The detected mode (PLAN, ACT, EVAL, AUTO). + state_file: Optional explicit path; uses default when None. + """ + try: + phase = _MODE_PHASE_MAP.get(mode, "ready") + kwargs = { + "currentMode": mode, + "phase": phase, + "focus": None, + "blockerCount": 0, + } + if state_file: + update_hud_state(state_file=state_file, **kwargs) + else: + update_hud_state(**kwargs) + except Exception: + pass + + +def on_tool_start( + tool_name: str, + tool_input: dict, + *, + state_file: Optional[str] = None, +) -> None: + """Update HUD when a tool invocation begins. + + Called from PreToolUse. Only updates when the information is + meaningfully stable (e.g. active agent change, MCP parse_mode). + + Args: + tool_name: Name of the tool being invoked. + tool_input: The tool_input dict from the hook payload. + state_file: Optional explicit path; uses default when None. + """ + try: + updates: dict = {} + + # Detect active agent from environment (set by parse_mode MCP) + agent = os.environ.get("CODINGBUDDY_ACTIVE_AGENT", "") + if agent: + updates["activeAgent"] = agent + + # Detect focus from meaningful tool patterns + focus = _detect_focus(tool_name, tool_input) + if focus is not None: + updates["focus"] = focus + + # Detect execution strategy from Agent/Task tool use + strategy = _detect_strategy(tool_name, tool_input) + if strategy is not None: + updates["executionStrategy"] = strategy + + if updates: + if state_file: + update_hud_state(state_file=state_file, **updates) + else: + update_hud_state(**updates) + except Exception: + pass + + +def on_tool_end( + tool_name: str, + tool_input: dict, + tool_output: str, + *, + state_file: Optional[str] = None, +) -> None: + """Record stable post-action state after a tool completes. + + Called from PostToolUse. Captures agent handoffs and phase + transitions that are evident from tool outputs. + + Args: + tool_name: Name of the completed tool. + tool_input: The tool_input dict from the hook payload. + tool_output: The tool_output string from the hook payload. + state_file: Optional explicit path; uses default when None. + """ + try: + updates: dict = {} + + # Track agent handoffs via environment changes + agent = os.environ.get("CODINGBUDDY_ACTIVE_AGENT", "") + if agent: + updates["activeAgent"] = agent + updates["lastHandoff"] = agent + + # Detect phase changes from parse_mode MCP calls + if tool_name == "mcp__codingbuddy__parse_mode": + mode = _extract_mode_from_parse_mode(tool_input) + if mode: + phase = _MODE_PHASE_MAP.get(mode, "ready") + updates["currentMode"] = mode + updates["phase"] = phase + + if updates: + if state_file: + update_hud_state(state_file=state_file, **updates) + else: + update_hud_state(**updates) + except Exception: + pass + + +def on_session_stop( + *, + state_file: Optional[str] = None, +) -> None: + """Clear active workflow state when the session ends. + + Called from Stop hook. + + Args: + state_file: Optional explicit path; uses default when None. + """ + try: + kwargs = { + "activeAgent": None, + "phase": "completed", + "focus": None, + "executionStrategy": None, + "councilStatus": None, + "blockerCount": 0, + } + if state_file: + update_hud_state(state_file=state_file, **kwargs) + else: + update_hud_state(**kwargs) + except Exception: + pass + + +def init_baseline( + pending_context: Optional[dict] = None, + *, + state_file: Optional[str] = None, +) -> None: + """Enrich the freshly-initialised HUD state with baseline context. + + Called from SessionStart *after* ``init_hud_state()``. If a pending + context.md was detected, seeds currentMode and phase so the status + bar immediately reflects the resuming session. + + Args: + pending_context: Dict with optional ``mode``/``status`` keys + from ``_read_pending_context()``. + state_file: Optional explicit path; uses default when None. + """ + if not pending_context: + return + + try: + mode = pending_context.get("mode") + if not mode: + return + + updates: dict = {"currentMode": mode} + phase = _MODE_PHASE_MAP.get(mode) + if phase: + updates["phase"] = phase + + if state_file: + update_hud_state(state_file=state_file, **updates) + else: + update_hud_state(**updates) + except Exception: + pass + + +# ---- private helpers ---- + + +def _detect_focus(tool_name: str, tool_input: dict) -> Optional[str]: + """Infer a human-readable focus label from the current tool call.""" + if tool_name == "Edit" or tool_name == "Write": + path = tool_input.get("file_path", "") + if path: + # Return just the filename for brevity + return os.path.basename(path) + + if tool_name == "Bash": + cmd = tool_input.get("command", "") + if cmd.startswith("git commit"): + return "committing" + if cmd.startswith("git push"): + return "pushing" + if "pytest" in cmd or "vitest" in cmd or "jest" in cmd: + return "testing" + if "yarn build" in cmd or "npm run build" in cmd: + return "building" + + if tool_name == "mcp__codingbuddy__parse_mode": + prompt = tool_input.get("prompt", "") + if prompt: + # First 40 chars of the prompt as focus + return prompt[:40].strip() + + return None + + +def _detect_strategy(tool_name: str, tool_input: dict) -> Optional[str]: + """Detect execution strategy from Agent/Task tool patterns.""" + if tool_name == "Agent": + return "subagent" + if tool_name == "Bash": + cmd = tool_input.get("command", "") + if "tmux" in cmd: + return "taskmaestro" + return None + + +def _extract_mode_from_parse_mode(tool_input: dict) -> Optional[str]: + """Extract the mode keyword from a parse_mode tool_input.""" + prompt = tool_input.get("prompt", "") + if not prompt: + return None + first_word = prompt.strip().split()[0].upper().rstrip(":") + valid_modes = {"PLAN", "ACT", "EVAL", "AUTO"} + return first_word if first_word in valid_modes else None diff --git a/packages/claude-code-plugin/hooks/post-tool-use.py b/packages/claude-code-plugin/hooks/post-tool-use.py index a2b2b2ed..250a9af8 100644 --- a/packages/claude-code-plugin/hooks/post-tool-use.py +++ b/packages/claude-code-plugin/hooks/post-tool-use.py @@ -56,6 +56,18 @@ def handle_post_tool_use(data: dict): except Exception: pass # Never block tool execution + # Update HUD state with post-action information (#1324) + try: + from hud_helpers import on_tool_end + + on_tool_end( + data.get("tool_name", ""), + data.get("tool_input", {}), + str(data.get("tool_output", "")), + ) + except Exception: + pass # Never block tool execution + return None diff --git a/packages/claude-code-plugin/hooks/pre-tool-use.py b/packages/claude-code-plugin/hooks/pre-tool-use.py index a14c8e91..ffc440af 100644 --- a/packages/claude-code-plugin/hooks/pre-tool-use.py +++ b/packages/claude-code-plugin/hooks/pre-tool-use.py @@ -237,6 +237,14 @@ def _handle(data: dict) -> Optional[dict]: if checklist_warning: contexts.append(checklist_warning) + # Update HUD state with active agent, focus, strategy (#1324) + try: + from hud_helpers import on_tool_start + + on_tool_start(tool_name, data.get("tool_input", {})) + except Exception: + pass + # Build response — include statusMessage and/or additionalContext if not status_msg and not contexts: return None diff --git a/packages/claude-code-plugin/hooks/session-start.py b/packages/claude-code-plugin/hooks/session-start.py index da83f367..e044f239 100644 --- a/packages/claude-code-plugin/hooks/session-start.py +++ b/packages/claude-code-plugin/hooks/session-start.py @@ -773,6 +773,7 @@ def main(): pass # Never block session start # Step 4.5: Initialize HUD state for statusLine (#1089) + _pending_ctx_for_hud = None try: _ensure_lib_path() @@ -784,6 +785,17 @@ def main(): except Exception: pass # Never block session start + # Step 4.5b: Enrich HUD baseline with pending context (#1324) + try: + cwd_hud = os.environ.get("CLAUDE_PROJECT_DIR", str(Path.cwd())) + _pending_ctx_for_hud = _read_pending_context(cwd_hud) + if _pending_ctx_for_hud: + from hud_helpers import init_baseline + + init_baseline(_pending_ctx_for_hud) + except Exception: + pass # Never block session start + # Step 4.6: Detect recent briefings and suggest recovery (#1125) try: _check_briefing_recovery() diff --git a/packages/claude-code-plugin/hooks/stop.py b/packages/claude-code-plugin/hooks/stop.py index b1254f6b..9006b2fd 100644 --- a/packages/claude-code-plugin/hooks/stop.py +++ b/packages/claude-code-plugin/hooks/stop.py @@ -117,6 +117,14 @@ def handle_stop(data: dict): except Exception: pass # Never block session stop + # Clear active HUD state (#1324) + try: + from hud_helpers import on_session_stop + + on_session_stop() + except Exception: + pass # Never block session stop + # End session in history database (#823) try: from history_db import HistoryDB diff --git a/packages/claude-code-plugin/hooks/tests/test_hud_helpers.py b/packages/claude-code-plugin/hooks/tests/test_hud_helpers.py new file mode 100644 index 00000000..4a7a26a5 --- /dev/null +++ b/packages/claude-code-plugin/hooks/tests/test_hud_helpers.py @@ -0,0 +1,377 @@ +"""Tests for hud_helpers module (#1324). + +Validates HUD state transitions driven by each hook lifecycle event: +SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop. +""" + +import json +import os +import sys +import tempfile + +import pytest + +# Ensure hooks/lib is importable +_hooks_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_lib_dir = os.path.join(_hooks_dir, "lib") +if _lib_dir not in sys.path: + sys.path.insert(0, _lib_dir) + +from hud_state import init_hud_state, read_hud_state +from hud_helpers import ( + init_baseline, + on_mode_entry, + on_tool_start, + on_tool_end, + on_session_stop, + _detect_focus, + _detect_strategy, + _extract_mode_from_parse_mode, +) + + +@pytest.fixture() +def state_file(tmp_path): + """Create a temp HUD state file, initialized with baseline state.""" + sf = str(tmp_path / "hud-state.json") + init_hud_state("test-session-123", "5.0.0", state_file=sf) + return sf + + +def _read(sf: str) -> dict: + """Read and return state from file.""" + return read_hud_state(sf, fill_defaults=True) + + +# ---- init_baseline ---- + +class TestInitBaseline: + """SessionStart: init_baseline enriches freshly-initialized state.""" + + def test_sets_mode_and_phase_from_pending_context(self, state_file): + init_baseline({"mode": "ACT"}, state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "ACT" + assert state["phase"] == "executing" + + def test_noop_when_no_pending_context(self, state_file): + init_baseline(None, state_file=state_file) + state = _read(state_file) + assert state["currentMode"] is None # unchanged from init + assert state["phase"] == "ready" + + def test_noop_when_pending_context_has_no_mode(self, state_file): + init_baseline({"status": "in_progress"}, state_file=state_file) + state = _read(state_file) + assert state["currentMode"] is None + + def test_plan_mode_from_context(self, state_file): + init_baseline({"mode": "PLAN"}, state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "PLAN" + assert state["phase"] == "planning" + + +# ---- on_mode_entry ---- + +class TestOnModeEntry: + """UserPromptSubmit: on_mode_entry resets workflow fields.""" + + def test_plan_mode_sets_phase_planning(self, state_file): + on_mode_entry("PLAN", state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "PLAN" + assert state["phase"] == "planning" + assert state["focus"] is None + assert state["blockerCount"] == 0 + + def test_act_mode_sets_phase_executing(self, state_file): + on_mode_entry("ACT", state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "ACT" + assert state["phase"] == "executing" + + def test_eval_mode_sets_phase_evaluating(self, state_file): + on_mode_entry("EVAL", state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "EVAL" + assert state["phase"] == "evaluating" + + def test_auto_mode_sets_phase_cycling(self, state_file): + on_mode_entry("AUTO", state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "AUTO" + assert state["phase"] == "cycling" + + def test_resets_focus_and_blockers(self, state_file): + # Set some values first + from hud_state import update_hud_state + update_hud_state(state_file=state_file, focus="old-file.py", blockerCount=3) + + on_mode_entry("PLAN", state_file=state_file) + state = _read(state_file) + assert state["focus"] is None + assert state["blockerCount"] == 0 + + def test_unknown_mode_defaults_to_ready(self, state_file): + on_mode_entry("UNKNOWN", state_file=state_file) + state = _read(state_file) + assert state["currentMode"] == "UNKNOWN" + assert state["phase"] == "ready" + + +# ---- on_tool_start ---- + +class TestOnToolStart: + """PreToolUse: on_tool_start updates active agent, focus, strategy.""" + + def test_sets_active_agent_from_env(self, state_file, monkeypatch): + monkeypatch.setenv("CODINGBUDDY_ACTIVE_AGENT", "Frontend Developer") + on_tool_start("Edit", {"file_path": "/src/app.tsx"}, state_file=state_file) + state = _read(state_file) + assert state["activeAgent"] == "Frontend Developer" + + def test_sets_focus_for_edit_tool(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_start("Edit", {"file_path": "/src/components/Button.tsx"}, state_file=state_file) + state = _read(state_file) + assert state["focus"] == "Button.tsx" + + def test_sets_focus_for_write_tool(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_start("Write", {"file_path": "/src/new-file.py"}, state_file=state_file) + state = _read(state_file) + assert state["focus"] == "new-file.py" + + def test_sets_focus_testing_for_pytest_command(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_start("Bash", {"command": "python -m pytest tests/ -v"}, state_file=state_file) + state = _read(state_file) + assert state["focus"] == "testing" + + def test_sets_focus_building_for_build_command(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_start("Bash", {"command": "yarn build"}, state_file=state_file) + state = _read(state_file) + assert state["focus"] == "building" + + def test_sets_strategy_subagent_for_agent_tool(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_start("Agent", {"prompt": "review code"}, state_file=state_file) + state = _read(state_file) + assert state["executionStrategy"] == "subagent" + + def test_sets_strategy_taskmaestro_for_tmux(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_start("Bash", {"command": "tmux split-window"}, state_file=state_file) + state = _read(state_file) + assert state["executionStrategy"] == "taskmaestro" + + def test_noop_when_no_meaningful_info(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + # Read tool with no matching patterns + old_state = _read(state_file) + on_tool_start("Read", {"file_path": "/src/app.py"}, state_file=state_file) + new_state = _read(state_file) + # updatedAt may change, compare meaningful fields + assert new_state["activeAgent"] == old_state["activeAgent"] + assert new_state["focus"] == old_state["focus"] + + +# ---- on_tool_end ---- + +class TestOnToolEnd: + """PostToolUse: on_tool_end records post-action state.""" + + def test_updates_agent_and_handoff(self, state_file, monkeypatch): + monkeypatch.setenv("CODINGBUDDY_ACTIVE_AGENT", "Security Specialist") + on_tool_end("Bash", {}, "", state_file=state_file) + state = _read(state_file) + assert state["activeAgent"] == "Security Specialist" + assert state["lastHandoff"] == "Security Specialist" + + def test_updates_mode_from_parse_mode(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + on_tool_end( + "mcp__codingbuddy__parse_mode", + {"prompt": "EVAL: review code quality"}, + "{}", + state_file=state_file, + ) + state = _read(state_file) + assert state["currentMode"] == "EVAL" + assert state["phase"] == "evaluating" + + def test_noop_when_no_agent_and_no_parse_mode(self, state_file, monkeypatch): + monkeypatch.delenv("CODINGBUDDY_ACTIVE_AGENT", raising=False) + old_state = _read(state_file) + on_tool_end("Read", {}, "", state_file=state_file) + new_state = _read(state_file) + assert new_state["activeAgent"] == old_state["activeAgent"] + + +# ---- on_session_stop ---- + +class TestOnSessionStop: + """Stop: on_session_stop clears active state.""" + + def test_clears_agent_and_sets_completed(self, state_file): + # Set up active state first + from hud_state import update_hud_state + update_hud_state( + state_file=state_file, + activeAgent="Frontend Developer", + phase="executing", + focus="app.tsx", + executionStrategy="subagent", + blockerCount=2, + ) + + on_session_stop(state_file=state_file) + state = _read(state_file) + + assert state["activeAgent"] is None + assert state["phase"] == "completed" + assert state["focus"] is None + assert state["executionStrategy"] is None + assert state["councilStatus"] is None + assert state["blockerCount"] == 0 + + def test_preserves_session_metadata(self, state_file): + on_session_stop(state_file=state_file) + state = _read(state_file) + # Session metadata should survive + assert state["sessionId"] == "test-session-123" + assert state["version"] == "5.0.0" + assert "sessionStartTimestamp" in state + + +# ---- private helpers ---- + +class TestDetectFocus: + def test_returns_filename_for_edit(self): + assert _detect_focus("Edit", {"file_path": "/a/b/c.py"}) == "c.py" + + def test_returns_filename_for_write(self): + assert _detect_focus("Write", {"file_path": "/x/y.ts"}) == "y.ts" + + def test_returns_testing_for_pytest(self): + assert _detect_focus("Bash", {"command": "python -m pytest"}) == "testing" + + def test_returns_testing_for_vitest(self): + assert _detect_focus("Bash", {"command": "npx vitest run"}) == "testing" + + def test_returns_building_for_yarn_build(self): + assert _detect_focus("Bash", {"command": "yarn build"}) == "building" + + def test_returns_committing_for_git_commit(self): + assert _detect_focus("Bash", {"command": "git commit -m 'fix'"}) == "committing" + + def test_returns_pushing_for_git_push(self): + assert _detect_focus("Bash", {"command": "git push origin main"}) == "pushing" + + def test_returns_none_for_unrecognized_tool(self): + assert _detect_focus("Glob", {"pattern": "*.py"}) is None + + def test_returns_none_for_generic_bash(self): + assert _detect_focus("Bash", {"command": "ls -la"}) is None + + def test_truncates_parse_mode_prompt(self): + result = _detect_focus( + "mcp__codingbuddy__parse_mode", + {"prompt": "PLAN: implement a very long feature description here that exceeds limit"}, + ) + assert len(result) <= 40 + + +class TestDetectStrategy: + def test_subagent_for_agent_tool(self): + assert _detect_strategy("Agent", {}) == "subagent" + + def test_taskmaestro_for_tmux_command(self): + assert _detect_strategy("Bash", {"command": "tmux new-session"}) == "taskmaestro" + + def test_none_for_regular_tool(self): + assert _detect_strategy("Edit", {}) is None + + def test_none_for_non_tmux_bash(self): + assert _detect_strategy("Bash", {"command": "git status"}) is None + + +class TestExtractModeFromParseMode: + def test_extracts_plan(self): + assert _extract_mode_from_parse_mode({"prompt": "PLAN: test"}) == "PLAN" + + def test_extracts_act(self): + assert _extract_mode_from_parse_mode({"prompt": "ACT: do it"}) == "ACT" + + def test_extracts_eval(self): + assert _extract_mode_from_parse_mode({"prompt": "EVAL: review"}) == "EVAL" + + def test_extracts_auto(self): + assert _extract_mode_from_parse_mode({"prompt": "AUTO: build"}) == "AUTO" + + def test_returns_none_for_no_mode(self): + assert _extract_mode_from_parse_mode({"prompt": "hello world"}) is None + + def test_returns_none_for_empty_prompt(self): + assert _extract_mode_from_parse_mode({"prompt": ""}) is None + + def test_returns_none_for_missing_prompt(self): + assert _extract_mode_from_parse_mode({}) is None + + +# ---- Full lifecycle transition test ---- + +class TestFullLifecycle: + """End-to-end test of HUD state through a complete session lifecycle.""" + + def test_session_lifecycle(self, tmp_path, monkeypatch): + sf = str(tmp_path / "hud-state.json") + + # 1. SessionStart: init + init_hud_state("lifecycle-test", "5.0.0", state_file=sf) + state = _read(sf) + assert state["phase"] == "ready" + assert state["currentMode"] is None + + # 2. SessionStart: baseline from pending context + init_baseline({"mode": "PLAN"}, state_file=sf) + state = _read(sf) + assert state["currentMode"] == "PLAN" + assert state["phase"] == "planning" + + # 3. UserPromptSubmit: new mode entry + on_mode_entry("ACT", state_file=sf) + state = _read(sf) + assert state["currentMode"] == "ACT" + assert state["phase"] == "executing" + assert state["focus"] is None + assert state["blockerCount"] == 0 + + # 4. PreToolUse: editing a file with agent active + monkeypatch.setenv("CODINGBUDDY_ACTIVE_AGENT", "Frontend Developer") + on_tool_start("Edit", {"file_path": "/src/App.tsx"}, state_file=sf) + state = _read(sf) + assert state["activeAgent"] == "Frontend Developer" + assert state["focus"] == "App.tsx" + + # 5. PostToolUse: agent handoff recorded + on_tool_end("Edit", {}, "", state_file=sf) + state = _read(sf) + assert state["lastHandoff"] == "Frontend Developer" + + # 6. PreToolUse: running tests + on_tool_start("Bash", {"command": "python -m pytest tests/"}, state_file=sf) + state = _read(sf) + assert state["focus"] == "testing" + + # 7. Stop: clear active state + on_session_stop(state_file=sf) + state = _read(sf) + assert state["activeAgent"] is None + assert state["phase"] == "completed" + assert state["focus"] is None + assert state["executionStrategy"] is None + # Session metadata survives + assert state["sessionId"] == "lifecycle-test" diff --git a/packages/claude-code-plugin/hooks/user-prompt-submit.py b/packages/claude-code-plugin/hooks/user-prompt-submit.py index 948f7810..0498b9f9 100644 --- a/packages/claude-code-plugin/hooks/user-prompt-submit.py +++ b/packages/claude-code-plugin/hooks/user-prompt-submit.py @@ -91,14 +91,14 @@ def main(): "call it for enhanced features." ) - # Update HUD state with detected mode (#1090) + # Update HUD state with detected mode and reset workflow fields (#1090, #1324) try: - from hud_state import update_hud_state + from hud_helpers import on_mode_entry state_file = os.environ.get("CODINGBUDDY_HUD_STATE_FILE") if state_file: - update_hud_state(state_file=state_file, currentMode=detected_mode) + on_mode_entry(detected_mode, state_file=state_file) else: - update_hud_state(currentMode=detected_mode) + on_mode_entry(detected_mode) except Exception: pass