mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f7335fc15 |
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: playwright-report
|
||||
path: tests/e2e/test-results/
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
|
||||
- name: Upload OpenHands logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: openhands-logs
|
||||
path: |
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
working-directory: ./frontend
|
||||
run: npx playwright test --project=chromium
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
context: containers/runtime
|
||||
- name: Upload runtime source for fork
|
||||
if: github.event.pull_request.head.repo.fork
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: runtime-src-${{ matrix.base_image.tag }}
|
||||
path: containers/runtime
|
||||
@@ -196,7 +196,7 @@ jobs:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/openhands/enterprise-server
|
||||
tags: |
|
||||
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload output.jsonl as artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
if: always() # Upload even if the previous steps fail
|
||||
with:
|
||||
name: resolver-output
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
echo "is_fork=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
if: steps.check-fork.outputs.is_fork == 'false'
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Check for .pr/ directory
|
||||
id: check
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
# Always checkout main branch for security - cannot test script changes in PRs
|
||||
- name: Checkout extensions repository
|
||||
if: steps.check-trace.outputs.trace_exists == 'true'
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: OpenHands/extensions
|
||||
path: extensions
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
--trace-file trace-info/laminar_trace_info.json
|
||||
|
||||
- name: Upload evaluation logs
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v5
|
||||
if: always() && steps.check-trace.outputs.trace_exists == 'true'
|
||||
with:
|
||||
name: pr-review-evaluation-${{ github.event.pull_request.number }}
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-openhands
|
||||
path: |
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-enterprise
|
||||
path: ".coverage.enterprise.${{ matrix.python_version }}"
|
||||
|
||||
@@ -1729,7 +1729,7 @@
|
||||
"syncMode": "IMPORT",
|
||||
"clientSecret": "$GITHUB_APP_CLIENT_SECRET",
|
||||
"caseSensitiveOriginalUsername": "false",
|
||||
"defaultScope": "openid email profile",
|
||||
"defaultScope": "openid email profile notifications",
|
||||
"baseUrl": "$GITHUB_BASE_URL"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
"""Shared Event router for OpenHands Server.
|
||||
|
||||
All endpoints in this router are unauthenticated — shared conversations are
|
||||
public. To avoid returning internal system state that the viewer does not
|
||||
need, ``ConversationStateUpdateEvent`` instances are filtered out before the
|
||||
response is sent. The shared-conversation frontend only renders messages,
|
||||
actions, observations, errors, and hook-execution events; state snapshots
|
||||
are consumed exclusively by the authenticated WebSocket path.
|
||||
"""
|
||||
"""Shared Event router for OpenHands Server."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
@@ -21,15 +13,9 @@ from server.sharing.shared_event_service import (
|
||||
from openhands.agent_server.models import EventPage, EventSortOrder
|
||||
from openhands.app_server.event_callback.event_callback_models import EventKind
|
||||
from openhands.sdk import Event
|
||||
from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
|
||||
from openhands.utils.environment import StorageProvider, get_storage_provider
|
||||
|
||||
|
||||
def _is_viewable(event: Event) -> bool:
|
||||
"""Return True if *event* should be included in public shared responses."""
|
||||
return not isinstance(event, ConversationStateUpdateEvent)
|
||||
|
||||
|
||||
def get_shared_event_service_injector() -> SharedEventServiceInjector:
|
||||
"""Get the appropriate SharedEventServiceInjector based on configuration.
|
||||
|
||||
@@ -102,7 +88,7 @@ async def search_shared_events(
|
||||
shared_event_service: SharedEventService = shared_event_service_dependency,
|
||||
) -> EventPage:
|
||||
"""Search / List events for a shared conversation."""
|
||||
page = await shared_event_service.search_shared_events(
|
||||
return await shared_event_service.search_shared_events(
|
||||
conversation_id=UUID(conversation_id),
|
||||
kind__eq=kind__eq,
|
||||
timestamp__gte=timestamp__gte,
|
||||
@@ -111,10 +97,6 @@ async def search_shared_events(
|
||||
page_id=page_id,
|
||||
limit=limit,
|
||||
)
|
||||
return EventPage(
|
||||
items=[e for e in page.items if _is_viewable(e)],
|
||||
next_page_id=page.next_page_id,
|
||||
)
|
||||
|
||||
|
||||
@router.get('/count')
|
||||
@@ -165,7 +147,7 @@ async def batch_get_shared_events(
|
||||
events = await shared_event_service.batch_get_shared_events(
|
||||
UUID(conversation_id), event_ids
|
||||
)
|
||||
return [e if e is not None and _is_viewable(e) else None for e in events]
|
||||
return events
|
||||
|
||||
|
||||
@router.get('/{conversation_id}/{event_id}')
|
||||
@@ -175,9 +157,6 @@ async def get_shared_event(
|
||||
shared_event_service: SharedEventService = shared_event_service_dependency,
|
||||
) -> Event | None:
|
||||
"""Get a single event from a shared conversation by conversation_id and event_id."""
|
||||
event = await shared_event_service.get_shared_event(
|
||||
return await shared_event_service.get_shared_event(
|
||||
UUID(conversation_id), UUID(event_id)
|
||||
)
|
||||
if event is not None and not _is_viewable(event):
|
||||
return None
|
||||
return event
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
"""Tests for ConversationStateUpdateEvent filtering in shared_event_router.
|
||||
|
||||
The shared-events endpoints are unauthenticated, so internal system state
|
||||
(ConversationStateUpdateEvent) must not be returned. The frontend shared-
|
||||
conversation viewer never renders these events — it only uses messages,
|
||||
actions, observations, errors, and hook-execution events.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from server.sharing.shared_event_router import (
|
||||
_is_viewable,
|
||||
batch_get_shared_events,
|
||||
get_shared_event,
|
||||
search_shared_events,
|
||||
)
|
||||
|
||||
from openhands.agent_server.models import EventPage
|
||||
from openhands.sdk.event.conversation_state import ConversationStateUpdateEvent
|
||||
from openhands.sdk.event.llm_convertible import MessageEvent
|
||||
from openhands.sdk.llm import Message, TextContent
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures / helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_message_event() -> MessageEvent:
|
||||
return MessageEvent(
|
||||
source='user',
|
||||
llm_message=Message(role='user', content=[TextContent(text='Hello')]),
|
||||
)
|
||||
|
||||
|
||||
def _make_state_event(
|
||||
key: str = 'full_state', value: dict | str = 'idle'
|
||||
) -> ConversationStateUpdateEvent:
|
||||
return ConversationStateUpdateEvent(key=key, value=value)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _is_viewable
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestIsViewable:
|
||||
def test_message_event_is_viewable(self):
|
||||
assert _is_viewable(_make_message_event()) is True
|
||||
|
||||
def test_full_state_event_is_not_viewable(self):
|
||||
assert _is_viewable(_make_state_event('full_state', {'agent': {}})) is False
|
||||
|
||||
def test_execution_status_event_is_not_viewable(self):
|
||||
assert _is_viewable(_make_state_event('execution_status', 'running')) is False
|
||||
|
||||
def test_stats_event_is_not_viewable(self):
|
||||
assert _is_viewable(_make_state_event('stats', {})) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# search_shared_events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSearchSharedEvents:
|
||||
@pytest.mark.asyncio
|
||||
async def test_filters_out_state_events(self):
|
||||
msg = _make_message_event()
|
||||
state = _make_state_event()
|
||||
mock_service = AsyncMock()
|
||||
mock_service.search_shared_events.return_value = EventPage(
|
||||
items=[msg, state, msg], next_page_id=None
|
||||
)
|
||||
|
||||
result = await search_shared_events(
|
||||
conversation_id=uuid4().hex,
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert len(result.items) == 2
|
||||
assert all(
|
||||
not isinstance(e, ConversationStateUpdateEvent) for e in result.items
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preserves_next_page_id(self):
|
||||
mock_service = AsyncMock()
|
||||
mock_service.search_shared_events.return_value = EventPage(
|
||||
items=[_make_state_event()], next_page_id='abc'
|
||||
)
|
||||
|
||||
result = await search_shared_events(
|
||||
conversation_id=uuid4().hex,
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert result.next_page_id == 'abc'
|
||||
assert result.items == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_page_unchanged(self):
|
||||
mock_service = AsyncMock()
|
||||
mock_service.search_shared_events.return_value = EventPage(
|
||||
items=[], next_page_id=None
|
||||
)
|
||||
|
||||
result = await search_shared_events(
|
||||
conversation_id=uuid4().hex,
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert result.items == []
|
||||
assert result.next_page_id is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# batch_get_shared_events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBatchGetSharedEvents:
|
||||
@pytest.mark.asyncio
|
||||
async def test_replaces_state_events_with_none(self):
|
||||
msg = _make_message_event()
|
||||
state = _make_state_event()
|
||||
mock_service = AsyncMock()
|
||||
mock_service.batch_get_shared_events.return_value = [msg, state, None]
|
||||
|
||||
result = await batch_get_shared_events(
|
||||
conversation_id=uuid4().hex,
|
||||
id=[uuid4().hex, uuid4().hex, uuid4().hex],
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert len(result) == 3
|
||||
assert result[0] is msg
|
||||
assert result[1] is None # state event replaced with None
|
||||
assert result[2] is None # originally None stays None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_shared_event
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetSharedEvent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_message_event(self):
|
||||
msg = _make_message_event()
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_shared_event.return_value = msg
|
||||
|
||||
result = await get_shared_event(
|
||||
conversation_id=uuid4().hex,
|
||||
event_id=uuid4().hex,
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert result is msg
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_for_state_event(self):
|
||||
state = _make_state_event()
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_shared_event.return_value = state
|
||||
|
||||
result = await get_shared_event(
|
||||
conversation_id=uuid4().hex,
|
||||
event_id=uuid4().hex,
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_not_found(self):
|
||||
mock_service = AsyncMock()
|
||||
mock_service.get_shared_event.return_value = None
|
||||
|
||||
result = await get_shared_event(
|
||||
conversation_id=uuid4().hex,
|
||||
event_id=uuid4().hex,
|
||||
shared_event_service=mock_service,
|
||||
)
|
||||
|
||||
assert result is None
|
||||
@@ -90,15 +90,14 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
// Mock the search function that's used by the dropdown
|
||||
vi.spyOn(GitService, "searchGitRepositories").mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
});
|
||||
vi.spyOn(GitService, "searchGitRepositories").mockResolvedValue(
|
||||
MOCK_RESPOSITORIES,
|
||||
);
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -128,8 +127,8 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
@@ -139,11 +138,14 @@ describe("RepoConnector", () => {
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
items: [
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
next_page_id: null,
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
// First select the provider
|
||||
@@ -197,8 +199,8 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
@@ -244,8 +246,8 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
@@ -288,8 +290,8 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
@@ -345,8 +347,8 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
@@ -361,11 +363,14 @@ describe("RepoConnector", () => {
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
items: [
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
next_page_id: null,
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
// First select the provider
|
||||
@@ -422,17 +427,20 @@ describe("RepoConnector", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
items: [
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
next_page_id: null,
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -187,7 +187,7 @@ describe("RepositorySelectionForm", () => {
|
||||
},
|
||||
];
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: { pages: [{ items: MOCK_REPOS }] },
|
||||
data: { pages: [{ data: MOCK_REPOS }] },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
hasNextPage: false,
|
||||
@@ -229,7 +229,7 @@ describe("RepositorySelectionForm", () => {
|
||||
|
||||
// Create a spy on the API call
|
||||
const searchGitReposSpy = vi.spyOn(GitService, "searchGitRepositories");
|
||||
searchGitReposSpy.mockResolvedValue({ items: MOCK_SEARCH_REPOS, next_page_id: null });
|
||||
searchGitReposSpy.mockResolvedValue(MOCK_SEARCH_REPOS);
|
||||
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: { pages: [] },
|
||||
@@ -267,7 +267,7 @@ describe("RepositorySelectionForm", () => {
|
||||
];
|
||||
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
data: { pages: [{ items: MOCK_SEARCH_REPOS }] },
|
||||
data: { pages: [{ data: MOCK_SEARCH_REPOS }] },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
hasNextPage: false,
|
||||
|
||||
@@ -115,8 +115,8 @@ describe("TaskCard", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -228,17 +228,20 @@ describe("HomeScreen", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
items: [
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
next_page_id: null,
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
renderHomeScreen();
|
||||
@@ -269,17 +272,20 @@ describe("HomeScreen", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
items: [
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
next_page_id: null,
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
renderHomeScreen();
|
||||
@@ -326,11 +332,14 @@ describe("HomeScreen", () => {
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
items: [
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
next_page_id: null,
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
// Select a repository to enable the repo launch button
|
||||
@@ -362,8 +371,8 @@ describe("HomeScreen", () => {
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
items: MOCK_RESPOSITORIES,
|
||||
next_page_id: null,
|
||||
data: MOCK_RESPOSITORIES,
|
||||
nextPage: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { isNumber } from "../../src/utils/is-number";
|
||||
|
||||
test("isNumber", () => {
|
||||
expect(isNumber(1)).toBe(true);
|
||||
expect(isNumber(0)).toBe(true);
|
||||
expect(isNumber("3")).toBe(true);
|
||||
expect(isNumber("0")).toBe(true);
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { describe, it, expect, vi, test } from "vitest";
|
||||
import {
|
||||
formatTimestamp,
|
||||
getExtension,
|
||||
removeApiKey,
|
||||
} from "../../src/utils/utils";
|
||||
import { getStatusText } from "#/utils/utils";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
@@ -17,6 +22,25 @@ const t = (key: string) => {
|
||||
return translations[key] || key;
|
||||
};
|
||||
|
||||
test("removeApiKey", () => {
|
||||
const data = [{ args: { LLM_API_KEY: "key", LANGUAGE: "en" } }];
|
||||
expect(removeApiKey(data)).toEqual([{ args: { LANGUAGE: "en" } }]);
|
||||
});
|
||||
|
||||
test("getExtension", () => {
|
||||
expect(getExtension("main.go")).toBe("go");
|
||||
expect(getExtension("get-extension.test.ts")).toBe("ts");
|
||||
expect(getExtension("directory")).toBe("");
|
||||
});
|
||||
|
||||
test("formatTimestamp", () => {
|
||||
const morningDate = new Date("2021-10-10T10:10:10.000").toISOString();
|
||||
expect(formatTimestamp(morningDate)).toBe("10/10/2021, 10:10:10");
|
||||
|
||||
const eveningDate = new Date("2021-10-10T22:10:10.000").toISOString();
|
||||
expect(formatTimestamp(eveningDate)).toBe("10/10/2021, 22:10:10");
|
||||
});
|
||||
|
||||
describe("getStatusText", () => {
|
||||
it("returns STOPPING when pausing", () => {
|
||||
const result = getStatusText({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { RepositoryPage, BranchPage, InstallationPage } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { GitRepository, PaginatedBranchesResponse, Branch } from "#/types/git";
|
||||
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
|
||||
import { GitChange, GitChangeDiff } from "../open-hands.types";
|
||||
import ConversationService from "../conversation-service/conversation-service.api";
|
||||
|
||||
@@ -10,122 +12,130 @@ class GitService {
|
||||
/**
|
||||
* Search for Git repositories
|
||||
* @param query Search query
|
||||
* @param provider Git provider to search in (required)
|
||||
* @param limit Number of results per page
|
||||
* @param pageId Cursor for pagination
|
||||
* @param installationId Filter by installation ID
|
||||
* @param sortOrder Sort order (asc or desc)
|
||||
* @returns Paginated repository response
|
||||
* @param per_page Number of results per page
|
||||
* @param selected_provider Git provider to search in
|
||||
* @returns List of matching repositories
|
||||
*/
|
||||
static async searchGitRepositories(
|
||||
query: string,
|
||||
provider: string,
|
||||
limit = 100,
|
||||
pageId?: string,
|
||||
installationId?: string,
|
||||
): Promise<RepositoryPage> {
|
||||
const { data } = await openHands.get<RepositoryPage>(
|
||||
"/api/v1/git/repositories/search",
|
||||
per_page = 100,
|
||||
selected_provider?: Provider,
|
||||
): Promise<GitRepository[]> {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/search/repositories",
|
||||
{
|
||||
params: {
|
||||
provider,
|
||||
query,
|
||||
limit,
|
||||
page_id: pageId,
|
||||
installation_id: installationId,
|
||||
per_page,
|
||||
selected_provider,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return data;
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user's Git repositories
|
||||
* @param provider Git provider
|
||||
* @param pageId Cursor for pagination
|
||||
* @param limit Number of results per page
|
||||
* @param installationId Filter by installation ID
|
||||
* @param sortOrder Sort order (asc or desc)
|
||||
* @param selected_provider Git provider
|
||||
* @param page Page number
|
||||
* @param per_page Number of results per page
|
||||
* @returns User's repositories with pagination info
|
||||
*/
|
||||
static async retrieveUserGitRepositories(
|
||||
provider: string,
|
||||
pageId?: string,
|
||||
limit = 30,
|
||||
installationId?: string,
|
||||
): Promise<RepositoryPage> {
|
||||
const { data } = await openHands.get<RepositoryPage>(
|
||||
"/api/v1/git/repositories/search",
|
||||
selected_provider: Provider,
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) {
|
||||
const { data } = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
provider,
|
||||
limit,
|
||||
page_id: pageId,
|
||||
installation_id: installationId,
|
||||
selected_provider,
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return data;
|
||||
const link =
|
||||
data.length > 0 && data[0].link_header ? data[0].link_header : "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
|
||||
return { data, nextPage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve repositories from a specific installation
|
||||
* @param provider Git provider
|
||||
* @param selected_provider Git provider
|
||||
* @param installationIndex Current installation index
|
||||
* @param installations List of installation IDs
|
||||
* @param pageId Cursor for pagination
|
||||
* @param limit Number of results per page
|
||||
* @param page Page number
|
||||
* @param per_page Number of results per page
|
||||
* @returns Installation repositories with pagination info
|
||||
*/
|
||||
static async retrieveInstallationRepositories(
|
||||
provider: string,
|
||||
selected_provider: Provider,
|
||||
installationIndex: number,
|
||||
installations: string[],
|
||||
pageId?: string,
|
||||
limit = 30,
|
||||
): Promise<RepositoryPage> {
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) {
|
||||
const installationId = installations[installationIndex];
|
||||
const { data } = await openHands.get<RepositoryPage>(
|
||||
"/api/v1/git/repositories/search",
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
provider,
|
||||
limit,
|
||||
page_id: pageId,
|
||||
selected_provider,
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
installation_id: installationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
const link =
|
||||
response.data.length > 0 && response.data[0].link_header
|
||||
? response.data[0].link_header
|
||||
: "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
let nextInstallation: number | null;
|
||||
if (nextPage) {
|
||||
nextInstallation = installationIndex;
|
||||
} else if (installationIndex + 1 < installations.length) {
|
||||
nextInstallation = installationIndex + 1;
|
||||
} else {
|
||||
nextInstallation = null;
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
nextPage,
|
||||
installationIndex: nextInstallation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository branches
|
||||
* @param repository Repository name
|
||||
* @param provider Git provider (required)
|
||||
* @param query Search query (required - can be empty string)
|
||||
* @param pageId Cursor for pagination
|
||||
* @param limit Number of results per page
|
||||
* @param page Page number
|
||||
* @param perPage Number of results per page
|
||||
* @returns Paginated branches response
|
||||
*/
|
||||
static async getRepositoryBranches(
|
||||
repository: string,
|
||||
provider: string,
|
||||
query: string = "",
|
||||
pageId?: string,
|
||||
limit = 30,
|
||||
): Promise<BranchPage> {
|
||||
const { data } = await openHands.get<BranchPage>(
|
||||
"/api/v1/git/branches/search",
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
selectedProvider?: Provider,
|
||||
): Promise<PaginatedBranchesResponse> {
|
||||
const { data } = await openHands.get<PaginatedBranchesResponse>(
|
||||
`/api/user/repository/branches`,
|
||||
{
|
||||
params: {
|
||||
provider,
|
||||
repository,
|
||||
query,
|
||||
page_id: pageId,
|
||||
limit,
|
||||
page,
|
||||
per_page: perPage,
|
||||
selected_provider: selectedProvider,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -135,51 +145,40 @@ class GitService {
|
||||
|
||||
/**
|
||||
* Search repository branches
|
||||
* @deprecated Use getRepositoryBranches instead - this method is identical
|
||||
* @param repository Repository name
|
||||
* @param provider Git provider (required)
|
||||
* @param query Search query
|
||||
* @param pageId Cursor for pagination
|
||||
* @param limit Number of results per page
|
||||
* @param perPage Number of results per page
|
||||
* @param selectedProvider Git provider
|
||||
* @returns List of matching branches
|
||||
*/
|
||||
static async searchRepositoryBranches(
|
||||
repository: string,
|
||||
provider: string,
|
||||
query: string,
|
||||
pageId?: string,
|
||||
limit = 30,
|
||||
): Promise<BranchPage> {
|
||||
return this.getRepositoryBranches(
|
||||
repository,
|
||||
provider,
|
||||
query,
|
||||
pageId,
|
||||
limit,
|
||||
perPage: number = 30,
|
||||
selectedProvider?: Provider,
|
||||
): Promise<Branch[]> {
|
||||
const { data } = await openHands.get<Branch[]>(
|
||||
`/api/user/search/branches`,
|
||||
{
|
||||
params: {
|
||||
repository,
|
||||
query,
|
||||
per_page: perPage,
|
||||
selected_provider: selectedProvider,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user installation IDs
|
||||
* @param provider The provider to get installation IDs for (github, bitbucket, etc.)
|
||||
* @param pageId Cursor for pagination
|
||||
* @param limit Max number of results
|
||||
* @returns Paginated installation response
|
||||
* @returns List of installation IDs
|
||||
*/
|
||||
static async getUserInstallations(
|
||||
provider: string,
|
||||
pageId?: string,
|
||||
limit = 100,
|
||||
): Promise<InstallationPage> {
|
||||
const { data } = await openHands.get<InstallationPage>(
|
||||
"/api/v1/git/installations/search",
|
||||
{
|
||||
params: {
|
||||
provider,
|
||||
page_id: pageId,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
static async getUserInstallationIds(provider: Provider): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>(
|
||||
`/api/user/installations?provider=${provider}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class SettingsService {
|
||||
* Get the settings from the server or use the default settings if not found
|
||||
*/
|
||||
static async getSettings(): Promise<Settings> {
|
||||
const { data } = await openHands.get<Settings>("/api/v1/settings");
|
||||
const { data } = await openHands.get<Settings>("/api/settings");
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class SettingsService {
|
||||
* @param settings - the settings to save
|
||||
*/
|
||||
static async saveSettings(settings: Partial<Settings>): Promise<boolean> {
|
||||
const data = await openHands.post("/api/v1/settings", settings);
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function useRepositoryData(
|
||||
|
||||
// Combine all repositories from paginated data
|
||||
const allRepositories = useMemo(
|
||||
() => repoData?.pages?.flatMap((page) => page.items) || [],
|
||||
() => repoData?.pages?.flatMap((page) => page.data) || [],
|
||||
[repoData],
|
||||
);
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ export function useUrlSearch(inputValue: string, provider: Provider) {
|
||||
try {
|
||||
const repositories = await GitService.searchGitRepositories(
|
||||
repoName,
|
||||
provider,
|
||||
3,
|
||||
provider,
|
||||
);
|
||||
|
||||
setUrlSearchResults(repositories.items);
|
||||
setUrlSearchResults(repositories);
|
||||
} catch {
|
||||
setUrlSearchResults([]);
|
||||
} finally {
|
||||
|
||||
@@ -6,9 +6,6 @@ import { useUserProviders } from "../use-user-providers";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { shouldUseInstallationRepos } from "#/utils/utils";
|
||||
|
||||
/**
|
||||
* Get the first page of app installations for the provider given.
|
||||
*/
|
||||
export const useAppInstallations = (selectedProvider: Provider | null) => {
|
||||
const { data: config } = useConfig();
|
||||
const { data: userIsAuthenticated } = useIsAuthed();
|
||||
@@ -16,7 +13,7 @@ export const useAppInstallations = (selectedProvider: Provider | null) => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["installations", providers || [], selectedProvider],
|
||||
queryFn: () => GitService.getUserInstallations(selectedProvider!),
|
||||
queryFn: () => GitService.getUserInstallationIds(selectedProvider!),
|
||||
enabled:
|
||||
userIsAuthenticated &&
|
||||
!!selectedProvider &&
|
||||
|
||||
@@ -30,11 +30,9 @@ export function useBranchData(
|
||||
provider,
|
||||
);
|
||||
|
||||
// Combine all branches from paginated data - use .items for V1 response
|
||||
// Combine all branches from paginated data
|
||||
const allBranches = useMemo(
|
||||
() =>
|
||||
branchData?.pages?.flatMap((page: { items: Branch[] }) => page.items) ||
|
||||
[],
|
||||
() => branchData?.pages?.flatMap((page) => page.branches) || [],
|
||||
[branchData],
|
||||
);
|
||||
|
||||
@@ -42,7 +40,7 @@ export function useBranchData(
|
||||
const defaultBranchInLoaded = useMemo(
|
||||
() =>
|
||||
defaultBranch
|
||||
? allBranches.find((branch: Branch) => branch.name === defaultBranch)
|
||||
? allBranches.find((branch) => branch.name === defaultBranch)
|
||||
: null,
|
||||
[allBranches, defaultBranch],
|
||||
);
|
||||
@@ -77,7 +75,7 @@ export function useBranchData(
|
||||
if (defaultBranch) {
|
||||
// Use the already computed defaultBranchInLoaded or check in current branches
|
||||
let defaultBranchObj = shouldUseSearch
|
||||
? branchesToUse.find((branch: Branch) => branch.name === defaultBranch)
|
||||
? branchesToUse.find((branch) => branch.name === defaultBranch)
|
||||
: defaultBranchInLoaded;
|
||||
|
||||
// If not found in current branches, check if we have it from the default branch search
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useInfiniteQuery, InfiniteData } from "@tanstack/react-query";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { useAppInstallations } from "./use-app-installations";
|
||||
import { RepositoryPage } from "../../types/git";
|
||||
import { GitRepository } from "../../types/git";
|
||||
import { Provider } from "../../types/settings";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { shouldUseInstallationRepos } from "#/utils/utils";
|
||||
@@ -13,27 +13,29 @@ interface UseGitRepositoriesOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
type InstallationCursor = { installationIndex: number; pageId: string | null };
|
||||
type UserCursor = string | null;
|
||||
type Cursor = InstallationCursor | UserCursor;
|
||||
interface UserRepositoriesResponse {
|
||||
data: GitRepository[];
|
||||
nextPage: number | null;
|
||||
}
|
||||
|
||||
interface InstallationRepositoriesResponse {
|
||||
data: GitRepository[];
|
||||
nextPage: number | null;
|
||||
installationIndex: number | null;
|
||||
}
|
||||
|
||||
export function useGitRepositories(options: UseGitRepositoriesOptions) {
|
||||
const { provider, pageSize = 30, enabled = true } = options;
|
||||
const { providers } = useUserProviders();
|
||||
const { data: config } = useConfig();
|
||||
const { data: page } = useAppInstallations(provider);
|
||||
const installations = page?.items;
|
||||
const { data: installations } = useAppInstallations(provider);
|
||||
|
||||
const useInstallationRepos = provider
|
||||
? shouldUseInstallationRepos(provider, config?.app_mode)
|
||||
: false;
|
||||
|
||||
const repos = useInfiniteQuery<
|
||||
RepositoryPage,
|
||||
Error,
|
||||
InfiniteData<RepositoryPage>,
|
||||
[string, string[], Provider | null, boolean, number, ...unknown[]],
|
||||
Cursor
|
||||
UserRepositoriesResponse | InstallationRepositoriesResponse
|
||||
>({
|
||||
queryKey: [
|
||||
"repositories",
|
||||
@@ -49,52 +51,56 @@ export function useGitRepositories(options: UseGitRepositoriesOptions) {
|
||||
}
|
||||
|
||||
if (useInstallationRepos) {
|
||||
const { repoPage, installationIndex } = pageParam as {
|
||||
installationIndex: number | null;
|
||||
repoPage: number | null;
|
||||
};
|
||||
|
||||
if (!installations) {
|
||||
throw new Error("Missing installation list");
|
||||
}
|
||||
|
||||
const cursor = pageParam as InstallationCursor;
|
||||
const result = await GitService.retrieveInstallationRepositories(
|
||||
return GitService.retrieveInstallationRepositories(
|
||||
provider,
|
||||
cursor.installationIndex,
|
||||
installationIndex || 0,
|
||||
installations,
|
||||
cursor.pageId ?? undefined,
|
||||
repoPage || 1,
|
||||
pageSize,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
const cursor = pageParam as UserCursor;
|
||||
const result = await GitService.retrieveUserGitRepositories(
|
||||
return GitService.retrieveUserGitRepositories(
|
||||
provider,
|
||||
cursor ?? undefined,
|
||||
pageParam as number,
|
||||
pageSize,
|
||||
);
|
||||
return result;
|
||||
},
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
||||
if (useInstallationRepos && installations) {
|
||||
// Installation-based pagination
|
||||
const currentCursor = lastPageParam as InstallationCursor;
|
||||
if (lastPage.next_page_id) {
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (useInstallationRepos) {
|
||||
const installationPage = lastPage as InstallationRepositoriesResponse;
|
||||
if (installationPage.nextPage) {
|
||||
return {
|
||||
installationIndex: currentCursor.installationIndex,
|
||||
pageId: lastPage.next_page_id,
|
||||
installationIndex: installationPage.installationIndex,
|
||||
repoPage: installationPage.nextPage,
|
||||
};
|
||||
}
|
||||
// Advance to next installation
|
||||
const nextInstallationIndex = currentCursor.installationIndex + 1;
|
||||
if (nextInstallationIndex < installations.length) {
|
||||
return { installationIndex: nextInstallationIndex, pageId: null };
|
||||
|
||||
if (installationPage.installationIndex !== null) {
|
||||
return {
|
||||
installationIndex: installationPage.installationIndex,
|
||||
repoPage: 1,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
|
||||
return null;
|
||||
}
|
||||
// User repositories pagination
|
||||
return lastPage.next_page_id;
|
||||
|
||||
const userPage = lastPage as UserRepositoriesResponse;
|
||||
return userPage.nextPage;
|
||||
},
|
||||
initialPageParam: useInstallationRepos
|
||||
? { installationIndex: 0, pageId: null }
|
||||
: null,
|
||||
? { installationIndex: 0, repoPage: 1 }
|
||||
: 1,
|
||||
enabled:
|
||||
enabled &&
|
||||
(providers || []).length > 0 &&
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import { useInfiniteQuery, InfiniteData } from "@tanstack/react-query";
|
||||
import { useQuery, useInfiniteQuery } from "@tanstack/react-query";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { BranchPage } from "#/types/git";
|
||||
import { Branch, PaginatedBranchesResponse } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export const useRepositoryBranches = (
|
||||
repository: string | null,
|
||||
selectedProvider?: Provider,
|
||||
) =>
|
||||
useQuery<Branch[]>({
|
||||
queryKey: ["repository", repository, "branches", selectedProvider],
|
||||
queryFn: async () => {
|
||||
if (!repository) return [];
|
||||
const response = await GitService.getRepositoryBranches(
|
||||
repository,
|
||||
1,
|
||||
30,
|
||||
selectedProvider,
|
||||
);
|
||||
// Ensure we return an array even if the response is malformed
|
||||
return Array.isArray(response.branches) ? response.branches : [];
|
||||
},
|
||||
enabled: !!repository,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
|
||||
export const useRepositoryBranchesPaginated = (
|
||||
repository: string | null,
|
||||
perPage: number = 30,
|
||||
selectedProvider?: Provider,
|
||||
) => {
|
||||
const result = useInfiniteQuery<
|
||||
BranchPage,
|
||||
Error,
|
||||
InfiniteData<BranchPage>,
|
||||
[string, string | null, ...unknown[]],
|
||||
string | null
|
||||
>({
|
||||
) =>
|
||||
useInfiniteQuery<PaginatedBranchesResponse, Error>({
|
||||
queryKey: [
|
||||
"repository",
|
||||
repository,
|
||||
@@ -23,29 +38,27 @@ export const useRepositoryBranchesPaginated = (
|
||||
perPage,
|
||||
selectedProvider,
|
||||
],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!repository || !selectedProvider) {
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
if (!repository) {
|
||||
return {
|
||||
items: [],
|
||||
next_page_id: null,
|
||||
branches: [],
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: perPage,
|
||||
total_count: 0,
|
||||
};
|
||||
}
|
||||
return GitService.getRepositoryBranches(
|
||||
repository,
|
||||
selectedProvider,
|
||||
"", // query (empty = list all)
|
||||
pageParam ?? undefined,
|
||||
pageParam as number,
|
||||
perPage,
|
||||
selectedProvider,
|
||||
);
|
||||
},
|
||||
enabled: !!repository && !!selectedProvider,
|
||||
enabled: !!repository,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.next_page_id ? lastPage.next_page_id : undefined,
|
||||
initialPageParam: null,
|
||||
// Use the has_next_page flag from the API response
|
||||
lastPage.has_next_page ? lastPage.current_page + 1 : undefined,
|
||||
initialPageParam: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,17 +20,15 @@ export function useSearchBranches(
|
||||
selectedProvider,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!repository || !query || !selectedProvider) return [];
|
||||
const response = await GitService.searchRepositoryBranches(
|
||||
if (!repository || !query) return [];
|
||||
return GitService.searchRepositoryBranches(
|
||||
repository,
|
||||
selectedProvider,
|
||||
query,
|
||||
undefined, // pageId
|
||||
perPage,
|
||||
selectedProvider,
|
||||
);
|
||||
return response.items;
|
||||
},
|
||||
enabled: !!repository && !!query && !!selectedProvider,
|
||||
enabled: !!repository && !!query,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
gcTime: 1000 * 60 * 15,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export function useSearchRepositories(
|
||||
@@ -9,20 +8,14 @@ export function useSearchRepositories(
|
||||
disabled?: boolean,
|
||||
pageSize: number = 100,
|
||||
) {
|
||||
// For backward compatibility, return the items array directly
|
||||
return useQuery<GitRepository[]>({
|
||||
return useQuery({
|
||||
queryKey: ["repositories", "search", query, selectedProvider, pageSize],
|
||||
queryFn: async () => {
|
||||
if (!selectedProvider) {
|
||||
return [];
|
||||
}
|
||||
const response = await GitService.searchGitRepositories(
|
||||
queryFn: () =>
|
||||
GitService.searchGitRepositories(
|
||||
query,
|
||||
selectedProvider, // provider (required)
|
||||
pageSize,
|
||||
);
|
||||
return response.items;
|
||||
},
|
||||
selectedProvider || undefined,
|
||||
),
|
||||
enabled: !!query && !!selectedProvider && !disabled,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
|
||||
@@ -106,6 +106,10 @@ export const SETTINGS_HANDLERS = [
|
||||
}),
|
||||
),
|
||||
|
||||
http.get("/api/options/agents", async () =>
|
||||
HttpResponse.json(["CodeActAgent", "CoActAgent"]),
|
||||
),
|
||||
|
||||
http.get("/api/options/security-analyzers", async () =>
|
||||
HttpResponse.json(["llm", "none"]),
|
||||
),
|
||||
@@ -141,7 +145,7 @@ export const SETTINGS_HANDLERS = [
|
||||
return HttpResponse.json(config);
|
||||
}),
|
||||
|
||||
http.get("/api/v1/settings", async () => {
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
@@ -150,7 +154,7 @@ export const SETTINGS_HANDLERS = [
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
|
||||
http.post("/api/v1/settings", async ({ request }) => {
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
await delay();
|
||||
const body = await request.json();
|
||||
|
||||
|
||||
Vendored
-24
@@ -30,30 +30,6 @@ interface PaginatedBranchesResponse {
|
||||
total_count?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API response for paginated branch search (cursor-based)
|
||||
*/
|
||||
interface BranchPage {
|
||||
items: Branch[];
|
||||
next_page_id: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API response for paginated repository search (cursor-based)
|
||||
*/
|
||||
interface RepositoryPage {
|
||||
items: GitRepository[];
|
||||
next_page_id: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* V1 API response for paginated installation search (cursor-based)
|
||||
*/
|
||||
interface InstallationPage {
|
||||
items: string[];
|
||||
next_page_id: string | null;
|
||||
}
|
||||
|
||||
interface GitRepository {
|
||||
id: string;
|
||||
full_name: string;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const isNumber = (value: string | number): boolean =>
|
||||
!Number.isNaN(Number(value));
|
||||
@@ -146,6 +146,28 @@ export const removeUnwantedKeys = (
|
||||
});
|
||||
};
|
||||
|
||||
export const removeApiKey = (
|
||||
data: EventActionHistory[],
|
||||
): EventActionHistory[] =>
|
||||
data.map((item) => {
|
||||
// Create a shallow copy of item
|
||||
const newItem = { ...item };
|
||||
|
||||
// Check if LLM_API_KEY exists and delete it from a new args object
|
||||
if (newItem.args?.LLM_API_KEY) {
|
||||
const newArgs = { ...newItem.args };
|
||||
delete newArgs.LLM_API_KEY;
|
||||
newItem.args = newArgs;
|
||||
}
|
||||
|
||||
return newItem;
|
||||
});
|
||||
|
||||
export const getExtension = (code: string) => {
|
||||
if (code.includes(".")) return code.split(".").pop() || "";
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get file extension from file name in uppercase format
|
||||
* @param fileName The file name to extract extension from
|
||||
@@ -161,6 +183,25 @@ export const getFileExtension = (fileName: string): string => {
|
||||
return extension || "FILE";
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a timestamp to a human-readable format
|
||||
* @param timestamp The timestamp to format (ISO 8601)
|
||||
* @returns The formatted timestamp
|
||||
*
|
||||
* @example
|
||||
* formatTimestamp("2021-10-10T10:10:10.000") // "10/10/2021, 10:10:10"
|
||||
* formatTimestamp("2021-10-10T22:10:10.000") // "10/10/2021, 22:10:10"
|
||||
*/
|
||||
export const formatTimestamp = (timestamp: string) =>
|
||||
new Date(timestamp).toLocaleString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
|
||||
export const shouldUseInstallationRepos = (
|
||||
provider: Provider,
|
||||
app_mode: "saas" | "oss" | undefined,
|
||||
|
||||
@@ -105,11 +105,8 @@ def get_default_permitted_cors_origins() -> list[str]:
|
||||
|
||||
|
||||
def get_openhands_provider_base_url() -> str | None:
|
||||
"""Return the base URL for the OpenHands provider, if configured.
|
||||
|
||||
Falls back to LLM_BASE_URL for backward compatibility.
|
||||
"""
|
||||
return os.getenv('OPENHANDS_PROVIDER_BASE_URL') or os.getenv('LLM_BASE_URL') or None
|
||||
"""Return the base URL for the OpenHands provider, if configured."""
|
||||
return os.getenv('OPENHANDS_PROVIDER_BASE_URL') or None
|
||||
|
||||
|
||||
def _get_default_lifespan():
|
||||
|
||||
@@ -241,30 +241,13 @@ async def search_branches(
|
||||
if decoded_page_id is not None:
|
||||
page = decoded_page_id
|
||||
|
||||
if query:
|
||||
if page != 1:
|
||||
# TODO(#13883): Support pagination for branch search after refactoring.
|
||||
# The search_branches method does not support paging in the same way as
|
||||
# get_branches - those should be merged into a single paginated method
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Pagination not yet supported for branch search queries. Use empty query to list all branches with pagination.',
|
||||
)
|
||||
# Get search results - we'll handle pagination ourselves
|
||||
branches: list[Branch] = await client.search_branches(
|
||||
selected_provider=provider,
|
||||
repository=repository,
|
||||
query=query,
|
||||
per_page=limit + 1,
|
||||
)
|
||||
else:
|
||||
current_page = await client.get_branches(
|
||||
repository=repository,
|
||||
specified_provider=provider,
|
||||
page=page,
|
||||
per_page=limit + 1,
|
||||
)
|
||||
branches = current_page.branches
|
||||
# Get search results - we'll handle pagination ourselves
|
||||
branches: list[Branch] = await client.search_branches(
|
||||
selected_provider=provider,
|
||||
repository=repository,
|
||||
query=query,
|
||||
per_page=limit + 1, # We'll handle pagination ourselves
|
||||
)
|
||||
|
||||
next_page_id = None
|
||||
if len(branches) > limit:
|
||||
|
||||
Reference in New Issue
Block a user