Skip to content

feat: handle hook rejection display using rejection_source and HookExecutionEvent#488

Open
xingyaoww wants to merge 14 commits into
mainfrom
feat/use-rejection-source-field
Open

feat: handle hook rejection display using rejection_source and HookExecutionEvent#488
xingyaoww wants to merge 14 commits into
mainfrom
feat/use-rejection-source-field

Conversation

@xingyaoww

@xingyaoww xingyaoww commented Feb 11, 2026

Copy link
Copy Markdown
Member

Summary

Update the CLI to properly display hook rejections by using both UserRejectObservation.rejection_source and HookExecutionEvent from the SDK. This covers both hook paths:

  • PreToolUse hooksUserRejectObservation(rejection_source="hook")
  • Stop hooksHookExecutionEvent(blocked=true)

All rejections use the standard error icon (✗) — no special icon.

image

Changes

TUI (openhands_cli/tui/widgets/richlog_visualizer.py)

  • Add _is_hook_rejection() helper: checks UserRejectObservation.rejection_source == "hook" or HookExecutionEvent.blocked
  • Add _get_rejection_title() helper for dynamic titles
  • Handle HookExecutionEvent: show "Hook Blocked Action" when blocked, "Hook Executed" when successful
  • Show "User Rejected Action" for user rejections
  • Color blocked HookExecutionEvent with error theme

ACP (openhands_cli/acp_impl/events/)

  • Add _is_hook_rejection() helper handling both HookExecutionEvent and UserRejectObservation
  • Add handle_hook_execution() to SharedEventHandler — prepends "Hook Blocked Action" header for blocked hooks, shows all hook executions
  • Dispatch HookExecutionEvent in both event.py and token_streamer.py

Tests

  • 9 tests for TUI hook rejection detection (TestHookRejectionDetection) — covers UserRejectObservation, HookExecutionEvent, and AgentErrorEvent
  • 7 tests for ACP hook rejection detection (TestACPHookRejectionDetection)
  • 3 snapshot tests for rejection event rendering (TestRejectionEventSnapshots)

Docs

  • Added headless/JSON E2E testing guide and SDK hook event flow documentation to AGENTS.md

Visual Example

Stop hook blocked (HookExecutionEvent):

Hook Blocked Action ✗

Stop hook passed (HookExecutionEvent):

Hook Executed ✓

PreToolUse hook blocked (UserRejectObservation):

Hook Blocked Action ✗

User rejection:

User Rejected Action ✗

Verification

  • make lint passed
  • make test passed (1293 tests)
  • make test-snapshots passed
  • E2E tested: ran CLI headless in SDK repo, stop hook correctly produced HookExecutionEvent with blocked: true

🚀 Try this PR

uvx --python 3.12 git+https://github.com/OpenHands/OpenHands-CLI.git@feat/use-rejection-source-field

Update the CLI to use the new rejection_source field from the SDK
to distinguish hook rejections from user rejections, instead of
pattern matching on rejection_reason strings.

This provides a cleaner, more reliable way to detect hook rejections.

TUI changes (richlog_visualizer.py):
- Add _is_hook_rejection() helper using getattr() for SDK compatibility
- Add _get_rejection_title() and _get_rejection_icon() helpers
- Show 'Hook Blocked Action' title and '⚡ ✗' icon for hook rejections
- Falls back to 'User Rejected Action' and '✗' for user rejections

ACP changes (shared_event_handler.py):
- Add _is_hook_rejection() helper using getattr() for SDK compatibility
- Prepend '**⚡ Hook Blocked Action**' header for hook rejections

Tests:
- Add 8 new tests for TUI hook rejection detection
- Add 5 new tests for ACP hook rejection detection
- Tests use model_copy() for SDK version compatibility

Depends on: OpenHands/software-agent-sdk#1995

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

github-actions Bot commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands_cli/acp_impl/events
   event.py53786%76, 80, 92, 128–129, 154–155
   shared_event_handler.py761382%125–128, 141, 143–145, 158, 202, 207, 209, 214
   token_streamer.py1261290%149, 161, 165, 168, 173, 175, 177, 179, 181, 246, 253, 273
openhands_cli/tui/widgets
   richlog_visualizer.py3607978%96, 100–102, 107–110, 112, 200, 207–208, 220, 265, 306, 388, 438–441, 454, 457–459, 462–464, 467, 469, 472, 506–510, 514, 521–524, 528, 540, 581, 586–587, 590–592, 595, 601–604, 606, 613, 627–628, 630, 636, 655–656, 659, 712, 715–716, 720, 743–745, 761, 779–780, 782, 802, 806, 827, 830, 848–849
TOTAL667092086% 

@openhands-ai

openhands-ai Bot commented Feb 11, 2026

Copy link
Copy Markdown

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Pre-commit checks

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #488 at branch `feat/use-rejection-source-field`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

@xingyaoww xingyaoww closed this Feb 11, 2026
@xingyaoww xingyaoww reopened this Feb 15, 2026
@xingyaoww

Copy link
Copy Markdown
Member Author

@OpenHands can you help me fix pre-commit run --all-files on this PR

@openhands-ai

openhands-ai Bot commented Feb 15, 2026

Copy link
Copy Markdown

I'm on it! xingyaoww can track my progress at all-hands.dev

@xingyaoww

Copy link
Copy Markdown
Member Author

@OpenHands review diff from main, and can you help me create a snapshot test for the rejection event feature?

@openhands-ai

openhands-ai Bot commented Feb 15, 2026

Copy link
Copy Markdown

I'm on it! xingyaoww can track my progress at all-hands.dev

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai

openhands-ai Bot commented Feb 15, 2026

Copy link
Copy Markdown

I've fixed the pre-commit issues on PR #488. The changes have been pushed to the feat/use-rejection-source-field branch.

What was fixed:

  • Removed an extra blank line between imports in tests/acp/events/test_shared_event_handler.py to pass the ruff lint check
  • Updated uv.lock to include the new pytest-rerunfailures dependency that was added in a recent merge from main

Verification:

  • pre-commit run --all-files now passes all checks:
    • ✅ Format YAML files
    • ✅ Ruff format
    • ✅ Ruff lint
    • ✅ PEP8 style check (pycodestyle)
    • ✅ Type check with pyright

The fix has been pushed to the PR branch and should now pass the pre-commit CI checks.

View full conversation

Add visual snapshot tests to verify the display of user and hook rejection events:
- test_user_rejection_display: verifies 'User Rejected Action' title
- test_hook_rejection_display: verifies 'Hook Blocked Action' title with ⚡ icon
- test_user_and_hook_rejections_comparison: shows both types side by side

These tests ensure the visual appearance of rejection events is correct and
provides regression protection for the rejection_source field feature.
@openhands-ai

openhands-ai Bot commented Feb 15, 2026

Copy link
Copy Markdown

Summary

I've reviewed the diff from main and created snapshot tests for the rejection event feature as requested. The changes have been pushed to the feat/use-rejection-source-field branch.

Changes Made

Added to tests/snapshots/test_visualizer_snapshots.py:

  • Added UserRejectObservation import
  • Added _create_user_reject_observation() helper function to create test rejection events
  • Added new TestRejectionEventSnapshots test class with 3 snapshot tests:
    • test_user_rejection_display: Verifies user rejection shows "User Rejected Action" title
    • test_hook_rejection_display: Verifies hook rejection shows "Hook Blocked Action" title with ⚡ icon
    • test_user_and_hook_rejections_comparison: Shows both rejection types side by side for visual comparison

Generated snapshot files:

  • TestRejectionEventSnapshots.test_user_rejection_display.svg
  • TestRejectionEventSnapshots.test_hook_rejection_display.svg
  • TestRejectionEventSnapshots.test_user_and_hook_rejections_comparison.svg

Verification

  • All 6 visualizer snapshot tests pass (3 existing + 3 new)
  • All 8 TUI hook rejection unit tests pass
  • All 5 ACP hook rejection unit tests pass

The snapshot tests provide visual regression protection for the rejection event feature, ensuring that:

  • User rejections display with "User Rejected Action" title
  • Hook rejections display with "Hook Blocked Action" title
  • The visual distinction between the two types is maintained

View full conversation

Comment thread openhands_cli/acp_impl/events/shared_event_handler.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.

@github-actions github-actions Bot added the Stale label Mar 28, 2026
@xingyaoww

Copy link
Copy Markdown
Member Author

@OpenHands Please merge from main, resolve all conflicts, and review all the PR review comments. Tell me if the implementation is correct. I think this PR we were basically waiting for a new SDK release so we can import stuff from the upstream SDK. Right Now, I think the latest main already updates to use the latest version of the SDK. So can you update this PR so to get it ready for me

@openhands-ai

openhands-ai Bot commented Mar 31, 2026

Copy link
Copy Markdown

I'm on it! xingyaoww can track my progress at all-hands.dev

- Merge from main (SDK 1.16.0 with rejection_source field)
- Replace getattr() workaround with direct event.rejection_source access
- Integrate hook rejection detection with main's refactored visualizer
- Update tests to use rejection_source parameter directly (no model_copy)
- Keep both hook rejection tests and new agent prefix tests

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai

openhands-ai Bot commented Mar 31, 2026

Copy link
Copy Markdown

Summary of Remaining Issue

The push triggered pre-commit checks, and two lint errors remain that need to be fixed:

Ruff Lint Failures (E501 - Line too long)

Both are the same issue — a docstring exceeding the 88-character line limit:

  1. tests/tui/widgets/test_richlog_visualizer.py:1282

    """Test _is_hook_rejection returns False when rejection_source defaults to 'user'."""
  2. tests/acp/events/test_shared_event_handler.py:36

    """Test _is_hook_rejection returns False when rejection_source defaults to 'user'."""

Additionally, ruff-format reformatted 1 file (likely auto-fixable formatting), which means the commit needs a follow-up fix.

What Was Completed ✅

  • Merged from main and resolved all 4 conflicts across 2 files
  • Reviewed the PR review comment — confirmed implementation is correct
  • Updated getattr() → direct event.rejection_source access (both TUI and ACP)
  • Updated tests to use rejection_source= parameter directly instead of model_copy()
  • All 1290 unit tests and 62 snapshot tests passed locally
  • Resolved the review thread on GitHub
  • Updated PR description

What Still Needs Fixing ❌

  • Shorten the two docstrings in test files to fit within the 88-char line limit
  • Run make lint to ensure ruff-format changes are committed

These are trivial one-line fixes — the docstrings just need to be shortened or wrapped. I should push a follow-up commit to fix these two lint errors so CI passes.

Co-authored-by: openhands <openhands@all-hands.dev>
@xingyaoww

Copy link
Copy Markdown
Member Author

horray! 85ff6bf <- this was due to the hook :D

@xingyaoww xingyaoww marked this pull request as ready for review March 31, 2026 11:07

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Good taste - Clean, simple solution that replaces fragile string parsing with proper field access.

Key Insight: Using a dedicated rejection_source field from the SDK instead of pattern matching is exactly the right data structure choice. The helper functions are simple, single-purpose, and eliminate special cases. Testing is thorough with real objects (not just mocks) and includes visual regression coverage.

Worth merging: Core logic is sound, implementation is clean, and tests are comprehensive.

Optional improvement: Consider adding a screenshot to the PR description showing the actual TUI displaying a hook rejection with the ⚡ icon (nice-to-have, not blocking).

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions github-actions Bot removed the Stale label Apr 1, 2026
Comment thread openhands_cli/tui/widgets/richlog_visualizer.py Outdated
Remove HOOK_ICON constant and _get_rejection_icon() function.
All rejection events now use the standard ERROR_ICON (✗).
Remove ⚡ from ACP HOOK_BLOCKED_HEADER.
Title distinction (Hook Blocked Action vs User Rejected Action) is preserved.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
The stop hook path uses HookExecutionEvent (not UserRejectObservation),
so 'Hook Blocked Action' was never shown for stop hook denials.

TUI: Import HookExecutionEvent, show 'Hook Blocked Action' collapsible
     for blocked HookExecutionEvents, hide successful ones.
ACP: Add handle_hook_execution() to SharedEventHandler, dispatch in
     event.py and token_streamer.py.
Tests: Add HookExecutionEvent tests for both TUI and ACP helpers.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Show all hook executions in TUI (as 'Hook Executed') and ACP,
not just blocked ones.

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@xingyaoww xingyaoww changed the title feat: use rejection_source field for hook rejection detection feat: handle hook rejection display using rejection_source and HookExecutionEvent Apr 2, 2026
@xingyaoww

Copy link
Copy Markdown
Member Author

Finally worked! @enyst

image

@xingyaoww xingyaoww requested review from all-hands-bot and enyst April 2, 2026 16:53

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Good taste - Clean solution that replaces fragile pattern matching with proper field access. The helper functions eliminate special cases, tests use real objects (not mocks), and the implementation is straightforward. Worth merging.

@enyst enyst removed their request for review May 11, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants