Skip to content

feat: Load workspace hooks for V1 conversations and add hooks viewer UI#12773

Merged
xingyaoww merged 63 commits intomainfrom
load-hooks-from-workspace
Mar 16, 2026
Merged

feat: Load workspace hooks for V1 conversations and add hooks viewer UI#12773
xingyaoww merged 63 commits intomainfrom
load-hooks-from-workspace

Conversation

@xingyaoww
Copy link
Copy Markdown
Collaborator

@xingyaoww xingyaoww commented Feb 6, 2026

Summary

This PR adds full workspace hooks support for V1 conversations — loading project-level hooks from .openhands/hooks.json at 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:

I'm testing hooks - Can you try to randomly modify one python file and add a really long line which will break pre-commit, then return agent finish action. Then, if you receive a follow-up message afterwards, fix that and tell me what you've done.
image image

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 HookConfig and the agent-server exposes /api/hooks to 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)

  1. Hook loading during conversation start (LiveStatusAppConversationService):

    • Added _load_hooks_from_workspace(...) which calls the agent-server /api/hooks endpoint to load hooks from the project directory.
    • _finalize_conversation_request(...) now loads hooks (when remote_workspace is available) and passes hook_config into StartConversationRequest.
    • Hook-loading failures degrade gracefully (hooks are optional; startup continues without them).
  2. Hook loading utility (openhands/app_server/app_conversation/hook_loader.py):

    • New module with load_hooks_from_agent_server(...) and get_project_dir_for_hooks(...) to keep the hook-loading logic consistent and reusable.
  3. Hooks viewer API (GET /{conversation_id}/hooks):

    • New endpoint that loads hooks live from the agent-server and returns them in a structured response format (GetHooksResponse with HookEventResponse, HookMatcherResponse, HookDefinitionResponse models).
  4. Refactored agent server context (_get_agent_server_context()):

    • Extracted the shared logic for resolving conversation → sandbox → sandbox spec → agent server URL into a reusable AgentServerContext dataclass + helper. This is now shared by both the skills and hooks endpoints, eliminating code duplication.
  5. Unit tests:

    • test_live_status_app_conversation_service.py – Tests for _load_hooks_from_workspace(...) and get_project_dir_for_hooks(...).
    • test_app_conversation_hooks_endpoint.py – Tests for the hooks API endpoint (success, no hooks found, 404 cases).

Frontend (React)

  1. Hook execution event rendering (V0 + V1):

    • New HookExecutionEventMessage component that renders hook execution details (command, exit code, stdout/stderr, blocked reason) in the chat timeline.
    • Added HookExecutionEvent TypeScript type, isHookExecutionEvent type guard, and source: "hook" support in the base event type.
    • Integrated into both frontend/src/components/features/chat/ (V0) and frontend/src/components/v1/chat/ (V1) event message renderers.
  2. Hooks modal (V1 only):

    • New HooksModal with collapsible event-type sections, matcher patterns, and command details.
    • Supporting components: HookEventItem, HookMatcherContent, HooksModalHeader, HooksEmptyState, HooksLoadingState.
  3. Context menu integration:

    • "Show Hooks" option added to both the ToolsContextMenu and ConversationNameContextMenu (V1 conversations only).
  4. Data fetching:

    • useConversationHooks() TanStack Query hook wrapping V1ConversationService.getHooks().
  5. i18n:

    • All new UI strings added to translation.json and declaration.ts for all supported languages.

How it works

Hook loading at conversation start:

  1. The app-server computes a project_dir:
    • No selected repo → {working_dir}
    • Selected repo → {working_dir}/{repo_name}
  2. The app-server calls the agent-server: POST /api/hooks with { "project_dir": "..." }
  3. The agent-server loads and parses {project_dir}/.openhands/hooks.json and returns hook_config
  4. The app-server passes hook_config to the agent-server via StartConversationRequest.hook_config

Hook 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

  • Unit tests for hook loading helpers and hooks API endpoint
  • Frontend test for hooks modal
  • Pre-commit hooks pass for modified files

SDK Dependency

This PR pins openhands-sdk, openhands-agent-server, and openhands-tools to an unreleased SDK commit (e68d1ee) from software-agent-sdk#2115 that adds the /api/hooks endpoint. The check-package-versions CI check is temporarily skipped for this branch until the SDK PR is merged and released.

Related PRs


To run this PR locally, use the following command:

GUI with Docker:

docker run -it --rm   -p 3000:3000   -v /var/run/docker.sock:/var/run/docker.sock   --add-host host.docker.internal:host-gateway   -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:9fc80b0-nikolaik   --name openhands-app-9fc80b0   docker.openhands.dev/openhands/openhands:9fc80b0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 6, 2026

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  openhands/app_server/app_conversation
  app_conversation_models.py
  app_conversation_router.py 80-82, 135-182, 591-618, 689-800
  hook_loader.py 37-40, 68-100, 127-148
  live_status_app_conversation_service.py 296-305, 1176, 1219-1255
Project Total  

This report was generated by python-coverage-comment-action

@xingyaoww xingyaoww force-pushed the load-hooks-from-workspace branch from 9a4531e to ce9089f Compare February 6, 2026 03:46
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>
@xingyaoww xingyaoww force-pushed the load-hooks-from-workspace branch from ce9089f to db5a274 Compare February 6, 2026 03:58
@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@OpenHands please fix the failing actions on PR #12773 at branch load-hooks-from-workspace

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"

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 8, 2026

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>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 8, 2026

Summary

1. Fixed the Failing Test ✅

The test test_finalize_conversation_request_skills_loading_fails was failing because:

  • The test expected exactly 1 warning call (mock_logger.warning.assert_called_once())
  • But the new hooks loading code added in this PR was also being executed and failing (due to unmocked remote_workspace.host attribute), causing 2 warning calls

Fix: Added a mock for _load_hooks_from_workspace to return None (no hooks found), isolating the test to focus specifically on skills loading failure behavior.

The fix has been pushed to the load-hooks-from-workspace branch.


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:

  1. Add a HookResponse model in app_conversation_models.py:
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
  1. Add a new endpoint in app_conversation_router.py:
@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 list

Frontend Changes Needed:

  1. Add API method in v1-conversation-service.api.ts:
static async getHooks(conversationId: string): Promise<GetHooksResponse> {
  const { data } = await openHands.get<GetHooksResponse>(
    `/api/v1/app-conversations/${conversationId}/hooks`,
  );
  return data;
}
  1. Create hook use-conversation-hooks.ts (copy from use-conversation-skills.ts)

  2. Create components (copy from skills components):

    • hooks-modal.tsx
    • hooks-modal-header.tsx
    • hook-item.tsx
    • hooks-empty-state.tsx
    • hooks-loading-state.tsx
  3. Add menu item in context menus (similar to "Show available skills")

The HookConfig structure has these event types you can display:

  • pre_tool_use - Hooks before tool execution
  • post_tool_use - Hooks after tool execution
  • user_prompt_submit - Hooks when user submits a prompt
  • session_start - Hooks when session starts
  • session_end - Hooks when session ends
  • stop - Hooks when agent attempts to stop

Each event type contains a list of HookMatcher objects with matcher (pattern) and hooks (list of commands).

View full conversation

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@OpenHands let's implement this #12773 (comment)

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 8, 2026

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>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 8, 2026

Summary

I'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 load-hooks-from-workspace branch.

Changes Made

Backend (Python):

  • Added HookDefinitionResponse, HookMatcherResponse, HookEventResponse, and GetHooksResponse models to app_conversation_models.py
  • Added GET /{conversation_id}/hooks endpoint to app_conversation_router.py that loads hooks from the workspace's .openhands/hooks.json file

Frontend (TypeScript/React):

  • Added getHooks() API method to v1-conversation-service.api.ts
  • Added HookDefinition, HookMatcher, HookEvent, GetHooksResponse types to v1-conversation-service.types.ts
  • Created use-conversation-hooks.ts React Query hook
  • Created hooks modal components:
    • hooks-modal.tsx - Main modal component
    • hooks-modal-header.tsx - Header with title and warning
    • hooks-loading-state.tsx - Loading spinner
    • hooks-empty-state.tsx - Empty state message
    • hook-event-item.tsx - Expandable event type item
    • hook-matcher-content.tsx - Matcher and command display
  • Added hooksModalVisible, setHooksModalVisible, handleShowHooks, and shouldShowHooks to use-conversation-name-context-menu.ts
  • Added "Show available hooks" menu item to both conversation-name-context-menu.tsx and tools-context-menu.tsx
  • Rendered HooksModal in conversation-name.tsx and tools.tsx
  • Added i18n translations for all new UI strings

Tests:

  • Updated tools-context-menu.test.tsx to include the new onShowHooks prop

Feature Behavior

  • The "Show available hooks" menu item appears only for V1 conversations (same as skills)
  • The modal displays hooks organized by event type (stop, pre_tool_use, post_tool_use, etc.)
  • Each event type shows matchers (patterns like *, exact match, or regex)
  • Each matcher shows the associated commands with type and timeout information
  • The modal includes a warning that changes require stopping and refreshing the conversation

Verification

  • All Python tests pass (86 tests)
  • TypeScript compilation passes
  • All pre-commit hooks pass

View full conversation

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@OpenHands please fix the failing actions on PR #12773 at branch load-hooks-from-workspace

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 9, 2026

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

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 9, 2026

Summary

I fixed the failing "Lint python" GitHub Action on PR #12773.

Issue

The pre-commit hook end-of-file-fixer was failing because frontend/src/i18n/translation.json was missing a newline at the end of the file.

Fix

Added the missing newline at the end of frontend/src/i18n/translation.json.

Changes

  • 1 file changed: frontend/src/i18n/translation.json - Added missing newline at end of file

Verification

  • ✅ Ran pre-commit hooks locally and confirmed they pass
  • ✅ Changes are minimal and directly address the lint failure
  • ✅ Pushed to load-hooks-from-workspace branch

The fix has been pushed to the PR branch and should resolve the failing lint check.

View full conversation

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@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

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 11, 2026

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>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 11, 2026

Summary

I've updated PR #12773 to point to the latest commit in OpenHands/software-agent-sdk that includes the /hooks API.

Changes Made

Updated pyproject.toml:

  • Changed openhands-agent-server, openhands-sdk, and openhands-tools from version 1.11.1 to use git commit 100e9af80d0d3d496f1f2c08d02bbd96b40863ce from the software-agent-sdk repository
  • Updated both project.dependencies (PEP 508 format) and tool.poetry.dependencies sections

Updated lock files:

  • poetry.lock and uv.lock regenerated to reflect the new git-based dependencies

Why This Was Needed

The /hooks API (POST /hooks endpoint in hooks_router.py) was added in PR #1878 (commit a506db1) on 2026-02-10, but the latest PyPI release (v1.11.2) was created on 2026-02-09 - before the hooks API was merged. By pointing to the latest commit on main (100e9af), the PR now has access to the /hooks API.

Verification

I verified the /hooks API is available:

from openhands.agent_server.hooks_router import hooks_router
print(hooks_router.routes)
# [APIRoute(path='/hooks', name='get_hooks', methods=['POST'])]

Checklist

  • ✅ Updated PR to point to commit in latest version of software-agent-sdk
  • ✅ Verified /hooks API is implemented (POST /hooks endpoint)
  • ✅ Changes are concise (only dependency updates)
  • ✅ Pushed to origin/load-hooks-from-workspace

The PR is now updated at: #12773

View full conversation

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@OpenHands pls merge from main and resolve all the conflicts

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 15, 2026

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>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 15, 2026

I've successfully merged main into the load-hooks-from-workspace branch and resolved all conflicts. Here's a summary:

Conflicts Resolved

  1. pyproject.toml - Resolved by accepting the released SDK versions (1.11.4) from main instead of the git commit references:

    • openhands-sdk = "1.11.4"
    • openhands-agent-server = "1.11.4"
    • openhands-tools = "1.11.4"
  2. poetry.lock - Accepted the version from main (which matches the released SDK versions)

  3. uv.lock - Accepted the version from main (which matches the released SDK versions)

Changes Pushed

The merge commit has been pushed to the load-hooks-from-workspace branch on GitHub. The PR (#12773) now includes all the latest changes from main while preserving the hooks loading functionality added in this PR.

The PR's core functionality (loading hooks from workspace .openhands/hooks.json and displaying them in "Show available hooks") remains intact with the 22 files changed adding ~1,075 lines of new code.

View full conversation

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@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

image

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Feb 15, 2026

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

Copy link
Copy Markdown
Collaborator

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

Choose a reason for hiding this comment

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

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.

Comment thread enterprise/Dockerfile Outdated
Comment thread openhands/app_server/app_conversation/live_status_app_conversation_service.py Outdated
Comment thread openhands/app_server/app_conversation/hook_loader.py
Comment thread openhands/app_server/app_conversation/hook_loader.py
Comment thread openhands/app_server/app_conversation/hook_loader.py
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>
@xingyaoww xingyaoww requested a review from all-hands-bot March 13, 2026 06:52
Copy link
Copy Markdown
Collaborator

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

Choose a reason for hiding this comment

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

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()
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.

🟢 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
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.

🟢 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();
});
});
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.

🟢 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.

@ak684
Copy link
Copy Markdown
Contributor

ak684 commented Mar 13, 2026

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 ────────────────────────────────────────────

  1. Medium: hook-load failures are surfaced as “no hooks configured”, not as failures. load_hooks_from_agent_server() swallows HTTP/status/parse errors and returns None at hook_loader.py:105, get_conversation_hooks() converts that None into a 200 {"hooks":[]} response at app_conversation_router.py:707, and the UI renders empty-state text instead of an error at hooks-empty-state.tsx:9. The branch’s own test suite codifies this behavior for an unreachable agent-server at test_app_conversation_hooks_endpoint.py:119. That makes “agent-server down / invalid hooks payload / hook parsing broke” indistinguishable from “repo has no hooks”.
  2. Medium: the UI offers “Show Available Hooks” for V1 conversations that cannot satisfy the endpoint. The menu gating only checks conversation_version === "V1" at use-conversation-name-context-menu.ts:280, and the modal considers every state except LOADING/INIT “ready” at hooks-modal.tsx:40. But the backend hard-requires a running sandbox and returns 404 otherwise at app_conversation_router.py:143. So paused/stopped/finished V1 conversations predictably open a feature that can only fail.
  3. Medium: blocked hook executions can render with success styling. In the SDK this branch targets, a hook that exits 0 but returns JSON { "decision": "deny" } or { "continue": false } stays success=True and is only marked blocked=True at executor.py:202; the event model also treats blocked as its own state at hook_execution.py:48. The frontend badge uses blocked, but the overall message status is derived only from event.success at hook-execution-event-message.tsx:63 and passed through to the generic renderer at generic-event-message.tsx:43. Result: a blocked hook can show “blocked” and a green success indicator simultaneously.

Not ready for merge as-is.

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@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

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Mar 13, 2026

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

Copy link
Copy Markdown
Collaborator Author

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" ✅ Real

Root cause: load_hooks_from_agent_server() in hook_loader.py catches all exceptions (HTTP errors, connection errors, parse errors) and returns None. The hooks endpoint in app_conversation_router.py then treats None the same as "no hooks found" — returning 200 {"hooks": []}. The frontend renders this as the empty state ("No hooks configured").

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 test_get_hooks_returns_empty_list_when_agent_server_unreachable codifies this behavior.

Note: The graceful degradation in load_hooks_from_agent_server() is correct for the conversation start path (hooks are optional and shouldn't block startup). However, the hooks viewer endpoint should surface errors so users know something went wrong.

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 ✅ Real

Root cause: The menu gating at use-conversation-name-context-menu.ts:280 only checks conversation_version === "V1", and the modal's isAgentReady check at hooks-modal.tsx:40 only excludes LOADING and INIT states. However, _get_agent_server_context() in the backend requires a running sandbox (returns 404 otherwise). So for finished/stopped/archived conversations, the menu option is visible but the endpoint will always fail.

The useConversationHooks query hook has a similar issue — it's enabled for any agent state except LOADING/INIT, so it fires for STOPPED conversations and gets a 404 back.

Fix: Add a conversationStatus === "RUNNING" check to the shouldShowHooks condition, so the menu option only appears when the sandbox is actually available.


Issue 3: Blocked hook executions render with success styling ✅ Real

Root cause: In hook-execution-event-message.tsx:140, the success prop passed to GenericEventMessage is derived solely from event.success:

success={event.success ? "success" : "error"}

When a hook exits 0 but is blocked (e.g., returns {"decision": "deny"}), event.success is true and event.blocked is true. This results in:

  • The title badge correctly shows "blocked" with amber styling (via getStatusClassName)
  • But the SuccessIndicator shows a green checkmark (because success="success")

These two indicators contradict each other.

Fix: When event.blocked is true, pass undefined instead of "success" to suppress the green checkmark, letting the amber "blocked" badge be the sole status indicator. The status badge in the title already handles blocked/success/error correctly.


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>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Mar 13, 2026

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 load-hooks-from-workspace:

  1. 98b96691f — Fix Issue 3: Blocked hooks showing success styling

    • In hook-execution-event-message.tsx, when event.blocked is true, the success prop passed to GenericEventMessage is now undefined instead of "success", suppressing the contradictory green checkmark alongside the amber "blocked" badge.
  2. 9529bf228 — Fix Issue 1: Hook-load failures indistinguishable from "no hooks"

    • Refactored hook_loader.py to expose fetch_hooks_from_agent_server() (raises on errors) alongside the existing load_hooks_from_agent_server() (swallows errors for graceful degradation at conversation start).
    • The hooks viewer endpoint in app_conversation_router.py now uses the error-raising variant and returns 502 Bad Gateway on HTTP/connection failures instead of 200 with empty hooks.
    • Updated test from expecting 200 to 502 for unreachable agent-server, and added a new test for HTTP status errors.
  3. 977150519 — Fix Issue 2: Show Hooks menu for stopped conversations

    • Added conversationStatus === "RUNNING" check to shouldShowHooks in use-conversation-name-context-menu.ts, so the menu option only appears when the sandbox is available to serve the request.

Checklist:

  • ✅ Each issue fixed in its own commit (as requested)
  • ✅ All 106 related tests pass
  • ✅ Pre-commit hooks pass for all changes
  • ✅ ESLint passes for modified frontend files
  • ✅ Comment posted on PR explaining the issues
  • ✅ Changes pushed to remote branch

@xingyaoww
Copy link
Copy Markdown
Collaborator Author

@OpenHands can you fix https://github.com/OpenHands/OpenHands/actions/runs/23052824363/job/66958143410?pr=12773

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Mar 13, 2026

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

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

openhands-ai Bot commented Mar 13, 2026

Summary

I fixed the failing "Lint python" GitHub Actions check on PR #12773 (load-hooks-from-workspace branch).

Root Cause: The ruff-format pre-commit hook failed because of a formatting issue in openhands/app_server/app_conversation/app_conversation_router.py. A short dictionary literal was spread across multiple lines when ruff expects it on a single line.

Fix (1 file, 1 line changed):

  • openhands/app_server/app_conversation/app_conversation_router.py — Collapsed the multi-line content={...} dict into a single line:
    # Before (3 lines):
    content={
        'error': 'Failed to reach agent-server when loading hooks'
    },
    
    # After (1 line):
    content={'error': 'Failed to reach agent-server when loading hooks'},

Commit: 0ead0ea5d — already pushed to origin/load-hooks-from-workspace.

CI Result: All 16 checks passed ✅ (1 skipped — end-to-end tests, which is expected for PRs).

PR link: #12773

@enyst
Copy link
Copy Markdown
Collaborator

enyst commented Mar 13, 2026

@OpenHands merge main branch into this PR branch and fix the conflicts. push.

@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Mar 13, 2026

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>
@openhands-ai
Copy link
Copy Markdown

openhands-ai Bot commented Mar 13, 2026

Final summary:

  • Merged main into load-hooks-from-workspace
  • Resolved the merge conflicts in the frontend i18n files
  • Preserved both sets of intended changes:
    • hook viewer / hook event UI strings from this PR
    • newer organization-management/payment-related i18n keys from main
  • Regenerated frontend/src/i18n/declaration.ts from the merged translation.json
  • Verified the result with:
    • npm run typecheck:staged
    • npm run build
    • poetry run pre-commit run --config ./dev_config/python/.pre-commit-config.yaml
  • Kept the final diff concise; no unrelated edits were left in the branch
  • Pushed the updated branch to remote

Pushed merge commit:

  • 39dad8d00Merge main into load-hooks-from-workspace

No additional changes remain beyond the merge/conflict resolution work.

@xingyaoww xingyaoww merged commit 00daaa4 into main Mar 16, 2026
18 checks passed
@xingyaoww xingyaoww deleted the load-hooks-from-workspace branch March 16, 2026 16:55
HeyItsChloe pushed a commit that referenced this pull request Mar 16, 2026
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>
@mamoodi mamoodi added the release:cloud-1.18.0 Released in Cloud 1.18.0 label Mar 26, 2026 — with OpenHands AI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:cloud-1.18.0 Released in Cloud 1.18.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants