Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions cp-agent/cp_agent/api/v1/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
MigrationRequest,
MigrationResponse,
ProjectCreateRequest,
SwitchCommitRequest,
SwitchCommitResponse,
)
from cp_agent.utils.agent_helpers import get_agent
from cp_agent.utils.project_download import create_project_zip
from cp_agent.utils.project_paths import find_project_paths
from cp_agent.utils.project_summary import generate_project_summary
from cp_agent.utils.snapshot import create_snapshot
from cp_agent.utils.runner_client import RunnerClient, ErrorModel
from cp_agent.utils.supabase_utils import SupabaseUtil

router = APIRouter()
Expand Down Expand Up @@ -579,6 +582,95 @@ async def download_project(
)


@router.post(
"/{project_id}/switch-commit",
response_model=SwitchCommitResponse,
status_code=status.HTTP_200_OK,
)
async def switch_commit(
project_id: str,
request: SwitchCommitRequest,
) -> SwitchCommitResponse:
"""Switch the project's working directory to a specific git commit.

Args:
project_id: The ID of the project.
request: Request containing the commit hash.

Returns:
SwitchCommitResponse: Indicates success or failure.

Raises:
HTTPException: 404 if project doesn't exist.
HTTPException: 500 if switching commit fails.
"""
# Check project exists (optional, runner might handle this)
project_manager = get_project_manager()
project = await project_manager.get_project(project_id)
Copy link

Copilot AI Apr 8, 2025

Choose a reason for hiding this comment

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

The function 'get_project_manager' is used but not imported or defined in this file. Please import or define 'get_project_manager' to avoid runtime errors.

Copilot uses AI. Check for mistakes.
if not project:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Project {project_id} not found",
)

runner_client: RunnerClient = RunnerClient()

try:
logger.info(
f"Attempting to switch project {project_id} to commit {request.commit_hash}"
)
# Call the runner using the updated client method
runner_response = await runner_client.switch_commit(
project_id=project_id, commit_hash=request.commit_hash
)

if isinstance(runner_response, ErrorModel):
logger.error(f"Runner failed to switch commit for project {project_id}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to switch commit",
)

logger.info(
f"Successfully switched project {project_id} to commit {request.commit_hash}"
)

# Add memory item and compact memory
try:
agent = await get_agent(project_id)
memory_message = (
"The project state has been reverted to an earlier point in the development history. "
"IMPORTANT: Any file contents previously accessed or stored in memory are now STALE. "
"You must re-read ALL files before working with them. "
)
await agent.message_manager.add_memory_item(memory_message)
logger.info(f"Added revert memory item for project {project_id}")
# Trigger compaction
await agent.message_manager.compact_memory()
logger.info(f"Triggered memory compaction for project {project_id}")
except Exception as agent_err:
# Log agent-related errors but don't fail the API call
logger.warning(
f"Error during agent memory update/compaction for project {project_id} after commit switch: {agent_err}"
)

return SwitchCommitResponse(
message="Commit switch successful",
success=True,
)

except HTTPException as http_exc:
raise http_exc
except Exception as e:
logger.exception(
f"Unexpected error switching commit for project {project_id}: {e}"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"An unexpected error occurred: {str(e)}",
)


def _cleanup_zip_file(zip_path: str) -> None:
"""Clean up the temporary zip file after it has been sent to the client.

Expand Down
13 changes: 13 additions & 0 deletions cp-agent/cp_agent/schemas/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ class ListProjectPathsResponse(BaseModel):
"""Response model for listing project paths."""

paths: List[str]


class SwitchCommitRequest(BaseModel):
"""Request model for switching git commit."""

commit_hash: str = Field(..., description="The git commit hash to switch to")


class SwitchCommitResponse(BaseModel):
"""Response model for switching git commit."""

message: str = Field(..., description="Success or error message")
success: bool = Field(..., description="Whether the switch was successful")
29 changes: 29 additions & 0 deletions cp-agent/cp_agent/utils/runner_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Client for interacting with cp-runner API."""

from cp_agent.config import settings
from cp_agent.generated.api.git import (
switch_commit as switch_commit_api,
)
from cp_agent.generated.api.projects import add_package as add_package_api
from cp_agent.generated.api.projects import check_build_errors
from cp_agent.generated.api.projects import lint_project as lint_project_api
Expand All @@ -17,11 +20,18 @@
from cp_agent.generated.models.project_operation_response_body import (
ProjectOperationResponseBody,
)
from cp_agent.generated.models.switch_commit_request_body import (
SwitchCommitRequestBody,
)
from cp_agent.generated.models.switch_commit_response_body import (
SwitchCommitResponseBody,
)

ResponseType = ErrorModel | ProjectOperationResponseBody
BuildErrorType = ErrorModel | BuildErrorResponseBody
LintResponseType = ErrorModel | LintResponseBody
AddPackageResponseType = ErrorModel | AddPackageResponseBody
SwitchCommitResponseType = ErrorModel | SwitchCommitResponseBody # Added type alias


class RunnerClient:
Expand Down Expand Up @@ -87,3 +97,22 @@ async def add_package(
return response
except Exception as e:
raise RuntimeError(f"Failed to install package: {e}")

async def switch_commit(
self, project_id: str, commit_hash: str
) -> SwitchCommitResponseType:
"""Switch the project's working directory to a specific commit via the runner."""
try:
request_body = SwitchCommitRequestBody(
project_id=project_id, commit_hash=commit_hash
)
response = await switch_commit_api.asyncio(
client=self.client, body=request_body
)

if not response:
raise RuntimeError("No response received from runner")

return response
except Exception as e:
raise RuntimeError(f"Failed to switch commit for project {project_id}: {e}")
10 changes: 7 additions & 3 deletions cp-webapp/src/components/snapshot-view/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React, { createContext, useContext, useState, ReactNode } from 'react';
import { useMutation } from '@tanstack/react-query';
import { switchCommitMutation } from '@/generated/runner/@tanstack/react-query.gen';
import { switchCommitApiV1ProjectsProjectIdSwitchCommitPostMutation } from '@/generated/agent/@tanstack/react-query.gen';
import { useToast } from '@/hooks/use-toast';
import { useProject } from '@/context';

Expand Down Expand Up @@ -35,7 +35,9 @@ export function SnapshotProvider({ children }: { children?: ReactNode }) {

const { toast } = useToast();

const switchCommitMutate = useMutation(switchCommitMutation());
const switchCommitMutate = useMutation(
switchCommitApiV1ProjectsProjectIdSwitchCommitPostMutation()
);

const triggerRefresh = () => {
setRefreshTrigger(prev => prev + 1);
Expand All @@ -47,8 +49,10 @@ export function SnapshotProvider({ children }: { children?: ReactNode }) {
try {
setSwitchingCommit(true);
await switchCommitMutate.mutateAsync({
body: {
path: {
project_id: projectId,
},
body: {
commit_hash: hash,
},
});
Expand Down
Loading
Loading