feat: Load workspace hooks for V1 conversations and add hooks viewer UI#12773
feat: Load workspace hooks for V1 conversations and add hooks viewer UI#12773
Conversation
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||
9a4531e to
ce9089f
Compare
This enables project-level hooks to be automatically loaded when starting a conversation in the V1 app_server, similar to how OpenHands-CLI loads hooks from the workspace. Changes: - Add hook_loader.py with load_hooks_from_agent_server function that calls the agent-server's /api/hooks endpoint (consistent with skill loading) - Add _load_hooks_from_workspace helper method in LiveStatusAppConversationService - Update _finalize_conversation_request to load hooks and pass hook_config to StartConversationRequest - Add unit tests for the new hook loading functionality This allows projects to define custom hooks (e.g., stop hooks for CI validation) that will be automatically loaded and executed during conversations. Requires: OpenHands/software-agent-sdk PR that adds /api/hooks endpoint Co-authored-by: openhands <openhands@all-hands.dev>
ce9089f to
db5a274
Compare
|
@OpenHands please fix the failing actions on PR #12773 at branch Also can you review diff against main, and tell me, is there a way to display the hooks in "Show available hooks"? You can just re-use most stuff for "Show available skills" and replace "skills" with "hooks" |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
The test_finalize_conversation_request_skills_loading_fails test was failing because the new hooks loading code was also being executed and failing (due to unmocked remote_workspace attributes), causing two warning calls instead of the expected one. This fix mocks _load_hooks_from_workspace to return None, isolating the test to focus specifically on skills loading failure behavior. Co-authored-by: openhands <openhands@all-hands.dev>
Summary1. Fixed the Failing Test ✅The test
Fix: Added a mock for The fix has been pushed to the 2. Answer: How to Display Hooks in "Show Available Hooks"Yes, you can absolutely implement "Show available hooks" by reusing the skills pattern! Here's how: Backend Changes Needed:
class HookResponse(BaseModel):
"""Response model for hooks endpoint."""
event_type: str # e.g., "stop", "pre_tool_use", "post_tool_use"
matcher: str # e.g., "*", "terminal", "/regex/"
command: str
timeout: int = 60
@router.get('/{conversation_id}/hooks')
async def get_conversation_hooks(conversation_id: UUID, ...) -> JSONResponse:
# Similar to get_conversation_skills:
# 1. Get conversation and sandbox info
# 2. Call agent-server's /api/hooks endpoint
# 3. Transform HookConfig to HookResponse listFrontend Changes Needed:
static async getHooks(conversationId: string): Promise<GetHooksResponse> {
const { data } = await openHands.get<GetHooksResponse>(
`/api/v1/app-conversations/${conversationId}/hooks`,
);
return data;
}
The
Each event type contains a list of |
|
@OpenHands let's implement this #12773 (comment) |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
This adds a new 'Show available hooks' menu item that displays hooks
configured in the workspace's .openhands/hooks.json file, similar to
the existing 'Show available skills' feature.
Backend changes:
- Add HookResponse models to app_conversation_models.py
- Add GET /{conversation_id}/hooks endpoint to app_conversation_router.py
Frontend changes:
- Add getHooks API method to v1-conversation-service.api.ts
- Add Hook types to v1-conversation-service.types.ts
- Create use-conversation-hooks.ts hook
- Create hooks modal components (hooks-modal.tsx, hook-event-item.tsx, etc.)
- Add hooks modal state and handler to use-conversation-name-context-menu.ts
- Add 'Show available hooks' menu item to context menus
- Add i18n translations for hooks modal
The hooks modal displays:
- Event types (pre_tool_use, post_tool_use, stop, etc.)
- Matchers (patterns like '*', exact match, or regex)
- Commands with type and timeout information
Co-authored-by: openhands <openhands@all-hands.dev>
SummaryI've successfully implemented the "Show available hooks" feature for the OpenHands UI, following the existing "Show available skills" pattern. The changes have been pushed to the Changes MadeBackend (Python):
Frontend (TypeScript/React):
Tests:
Feature Behavior
Verification
|
|
@OpenHands please fix the failing actions on PR #12773 at branch |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
SummaryI fixed the failing "Lint python" GitHub Action on PR #12773. IssueThe pre-commit hook FixAdded the missing newline at the end of Changes
Verification
The fix has been pushed to the PR branch and should resolve the failing lint check. |
|
@OpenHands can you update this PR to point to commit in the latest version of OpenHands/software-agent-sdk and make sure /hooks API is implemented there |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
Update openhands-agent-server, openhands-sdk, and openhands-tools to use git commit 100e9af from software-agent-sdk which includes the /hooks API endpoint (POST /hooks) for loading hooks from workspace .openhands/hooks.json files. This commit is after v1.11.2 and includes PR #1878 which added: - hooks_router.py with POST /hooks endpoint - hooks_service.py for loading hooks from workspace Co-authored-by: openhands <openhands@all-hands.dev>
SummaryI've updated PR #12773 to point to the latest commit in OpenHands/software-agent-sdk that includes the Changes MadeUpdated
Updated lock files:
Why This Was NeededThe VerificationI verified the from openhands.agent_server.hooks_router import hooks_router
print(hooks_router.routes)
# [APIRoute(path='/hooks', name='get_hooks', methods=['POST'])]Checklist
The PR is now updated at: #12773 |
|
@OpenHands pls merge from main and resolve all the conflicts |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
Resolved conflicts in pyproject.toml, poetry.lock, and uv.lock by accepting the released SDK versions (1.11.4) from main instead of git commit references. Co-authored-by: openhands <openhands@all-hands.dev>
|
I've successfully merged main into the Conflicts Resolved
Changes PushedThe merge commit has been pushed to the The PR's core functionality (loading hooks from workspace |
|
@OpenHands It does NOT display hooks when we've open up OpenHands/software-agent-sdk repo which should have ONE hook there. Can you help me fix it
|
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟡 Acceptable - Solid architecture with clean separation of concerns, but address the critical security issue and unresolved logger feedback before merging.
Verdict: ❌ Needs rework - Fix the hash removal and logger levels first.
Key Insight: The hook loading architecture is well-designed with proper graceful degradation, but the Docker hash removal undermines supply chain security without clear justification.
Reverts the --without-hashes and git package changes since we no longer need to pin to a particular SDK commit via VCS git dependencies. This restores supply chain security by keeping hash verification. Co-authored-by: openhands <openhands@all-hands.dev>
Move the duplicated HookExecutionEventMessage component from both V0 (features/chat) and V1 (v1/chat) to a shared location at components/shared/hook-execution-event-message.tsx. Both V0 and V1 now re-export from the shared module. Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste Rating: 🟢 Good taste - Clean architecture with solid engineering fundamentals.
Verdict: ✅ Approve - This is production-ready.
Key Insight: The _get_agent_server_context refactoring demonstrates good taste by eliminating code duplication between skills and hooks endpoints. Hook loading implements proper graceful degradation, ensuring conversation startup never fails due to missing hooks. The entire implementation follows existing patterns and handles edge cases correctly.
| @@ -92,6 +98,96 @@ async def anext(async_iterator): | |||
| sandbox_service_dependency = depends_sandbox_service() | |||
| sandbox_spec_service_dependency = depends_sandbox_spec_service() | |||
There was a problem hiding this comment.
🟢 Acceptable: Excellent refactoring. Extracting the shared agent server context logic eliminates duplication and makes both the skills and hooks endpoints more maintainable. This is exactly the kind of pragmatic cleanup that improves code quality without over-engineering.
| if selected_repository: | ||
| repo_name = selected_repository.split('/')[-1] | ||
| return f'{working_dir}/{repo_name}' | ||
| return working_dir |
There was a problem hiding this comment.
🟢 Acceptable: Smart design choice to compute project_dir at this layer rather than deep in the loading logic. Makes the intent clear and keeps the API simple.
| ); | ||
| expect(screen.getByText("unknown_event")).toBeInTheDocument(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🟢 Acceptable: Comprehensive test coverage for the modal component. Tests verify real UI behavior (rendering, interactions, state transitions) rather than just mocking internals. The i18n mocking is standard practice for component tests.
|
Hey @xingyaoww! I pointed GPT 5.4 xhigh at this branch locally and it called out the following "medium" issues. I haven't finish going through all 2,300 lines of code just yet but curious what you generally think of the feedback: ─ Worked for 1m 28s ────────────────────────────────────────────
Not ready for merge as-is. |
|
@OpenHands can you reflect on these comments: #12773 (comment) Are they real? If so, please post a comment, explain them in more details, and fix them. Make sure you fix each issue in their own commit - Do not put a fixes on different issues on the same commit |
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
|
Thanks @ak684 for the thorough review! I've analyzed all three issues and they are all real. Here's a detailed breakdown: Issue 1: Hook-load failures surfaced as "no hooks configured" ✅ RealRoot cause: This means when the agent-server is down, returns an error, or the hooks payload is malformed, the user sees "No hooks configured" instead of an error message. The test Note: The graceful degradation in Fix: Refactor the hooks endpoint to call the agent-server directly (with proper error handling) rather than reusing the error-swallowing helper. On HTTP/connection errors, return a 502 status so the frontend can distinguish "no hooks" from "load failed" and show the appropriate error state. Issue 2: UI offers "Show Available Hooks" for stopped V1 conversations ✅ RealRoot cause: The menu gating at The Fix: Add a Issue 3: Blocked hook executions render with success styling ✅ RealRoot cause: In success={event.success ? "success" : "error"}When a hook exits 0 but is blocked (e.g., returns
These two indicators contradict each other. Fix: When I'll fix each issue in its own commit. |
When a hook exits 0 but returns a deny/block decision, both event.success and event.blocked are true. Previously, the GenericEventMessage received success='success' derived solely from event.success, causing it to display a green checkmark alongside the amber 'blocked' badge — a contradictory UI state. Now, when event.blocked is true, the success indicator is suppressed (undefined), letting the amber 'blocked' badge be the sole status signal. Non-blocked hooks continue to show the green checkmark (success) or no indicator (error) as before. Co-authored-by: openhands <openhands@all-hands.dev>
…ing them Previously, the hooks viewer endpoint reused load_hooks_from_agent_server() which swallows all HTTP/connection/parse errors and returns None. The endpoint then treated None as 'no hooks found', returning 200 with empty hooks. This made agent-server failures indistinguishable from 'no hooks configured' in the UI. Refactored hook_loader.py to expose two functions: - fetch_hooks_from_agent_server(): raises on HTTP/connection errors - load_hooks_from_agent_server(): wraps the above, swallowing errors (still used for conversation start where hooks are optional) The hooks viewer endpoint now uses the error-raising variant and returns 502 Bad Gateway on HTTP/connection failures, so the frontend can show an error state instead of 'no hooks configured'. Updated tests to expect 502 for unreachable/erroring agent-server, and added a new test for HTTP status errors. Co-authored-by: openhands <openhands@all-hands.dev>
The hooks viewer endpoint requires a running sandbox to fetch hooks from the agent-server. Previously, the 'Show Hooks' menu option was shown for all V1 conversations regardless of status, so paused/stopped/ finished conversations would always open a modal that could only fail with a 404 error. Now shouldShowHooks additionally checks conversationStatus === 'RUNNING', so the menu option only appears when the sandbox is available. Co-authored-by: openhands <openhands@all-hands.dev>
|
All three issues from the PR comment have been analyzed, confirmed as real, and fixed. Here's a summary: Comment posted: Detailed analysis on PR #12773 explaining each issue's root cause and fix approach. Three commits pushed to
Checklist:
|
|
I'm on it! xingyaoww can track my progress at all-hands.dev |
Co-authored-by: openhands <openhands@all-hands.dev>
SummaryI fixed the failing "Lint python" GitHub Actions check on PR #12773 ( Root Cause: The Fix (1 file, 1 line changed):
Commit: CI Result: All 16 checks passed ✅ (1 skipped — end-to-end tests, which is expected for PRs). PR link: #12773 |
|
@OpenHands merge main branch into this PR branch and fix the conflicts. push. |
|
I'm on it! enyst can track my progress at all-hands.dev |
Resolve i18n merge conflicts and regenerate declarations. Co-authored-by: openhands <openhands@all-hands.dev>
|
Final summary:
Pushed merge commit:
No additional changes remain beyond the merge/conflict resolution work. |
When opening a previously closed conversation, the skills endpoint was returning 404 because the sandbox is no longer running. This was a regression introduced in PR #12773 which refactored the endpoint to use _get_agent_server_context helper that requires a running sandbox. This fix changes the behavior to return an empty skills list (200 OK) instead of 404 when: - Sandbox is not running - No exposed URLs available - Agent server URL not found This provides graceful degradation for closed conversations while maintaining the original error (404) only for truly not found conversations. Co-authored-by: openhands <openhands@all-hands.dev>

Summary
This PR adds full workspace hooks support for V1 conversations — loading project-level hooks from
.openhands/hooks.jsonat conversation start, exposing them via a new API endpoint, rendering hook execution events in the chat UI, and providing a "Show Hooks" modal for inspecting configured hooks.Screenshot that it is working
Test procedure: Select repo "OpenHands/software-agent-sdk" and enter this prompt:
Background
This change builds on OpenHands/software-agent-sdk#1878, which adds a project-level stop hook for pre-commit / pytest / CI validation. The SDK already supports hooks via
HookConfigand the agent-server exposes/api/hooksto load hooks from the workspace. However, the main OpenHands app_server was not loading hooks from the workspace when creating V1 conversations, and there was no way to view or inspect hook execution in the UI.Changes
Backend (Python)
Hook loading during conversation start (
LiveStatusAppConversationService):_load_hooks_from_workspace(...)which calls the agent-server/api/hooksendpoint to load hooks from the project directory._finalize_conversation_request(...)now loads hooks (whenremote_workspaceis available) and passeshook_configintoStartConversationRequest.Hook loading utility (
openhands/app_server/app_conversation/hook_loader.py):load_hooks_from_agent_server(...)andget_project_dir_for_hooks(...)to keep the hook-loading logic consistent and reusable.Hooks viewer API (
GET /{conversation_id}/hooks):GetHooksResponsewithHookEventResponse,HookMatcherResponse,HookDefinitionResponsemodels).Refactored agent server context (
_get_agent_server_context()):AgentServerContextdataclass + helper. This is now shared by both the skills and hooks endpoints, eliminating code duplication.Unit tests:
test_live_status_app_conversation_service.py– Tests for_load_hooks_from_workspace(...)andget_project_dir_for_hooks(...).test_app_conversation_hooks_endpoint.py– Tests for the hooks API endpoint (success, no hooks found, 404 cases).Frontend (React)
Hook execution event rendering (V0 + V1):
HookExecutionEventMessagecomponent that renders hook execution details (command, exit code, stdout/stderr, blocked reason) in the chat timeline.HookExecutionEventTypeScript type,isHookExecutionEventtype guard, andsource: "hook"support in the base event type.frontend/src/components/features/chat/(V0) andfrontend/src/components/v1/chat/(V1) event message renderers.Hooks modal (V1 only):
HooksModalwith collapsible event-type sections, matcher patterns, and command details.HookEventItem,HookMatcherContent,HooksModalHeader,HooksEmptyState,HooksLoadingState.Context menu integration:
ToolsContextMenuandConversationNameContextMenu(V1 conversations only).Data fetching:
useConversationHooks()TanStack Query hook wrappingV1ConversationService.getHooks().i18n:
translation.jsonanddeclaration.tsfor all supported languages.How it works
Hook loading at conversation start:
project_dir:{working_dir}{working_dir}/{repo_name}POST /api/hookswith{ "project_dir": "..." }{project_dir}/.openhands/hooks.jsonand returnshook_confighook_configto the agent-server viaStartConversationRequest.hook_configHook execution visibility:
When a hook executes during a conversation, the agent-server emits
HookExecutionEvents which are rendered in the chat timeline showing the command, exit code, output, and whether the hook blocked the action.Example hooks.json
{ "hooks": { "stop": [ { "matcher": "*", "hooks": [ { "type": "command", "command": ".openhands/hooks/on_stop.sh" } ] } ] } }Testing
SDK Dependency
This PR pins
openhands-sdk,openhands-agent-server, andopenhands-toolsto an unreleased SDK commit (e68d1ee) from software-agent-sdk#2115 that adds the/api/hooksendpoint. Thecheck-package-versionsCI check is temporarily skipped for this branch until the SDK PR is merged and released.Related PRs
/api/hooksendpointTo run this PR locally, use the following command:
GUI with Docker: