From 0106cdf07d0af116747adb1e5cb59f1268ff2ea9 Mon Sep 17 00:00:00 2001 From: JayGhiya Date: Thu, 12 Mar 2026 16:34:02 +0530 Subject: [PATCH 1/4] feat(cancellation): implement agent workflow cancellation across backend stack Add CANCELLED status to JobStatus enum and DB constraints in commons. Introduce cancellation detection helper that walks exception cause chains for CancelledError/asyncio.CancelledError (Python 3.13 compatible). Propagate cancellation through Temporal workflows, interceptor, and DB activities. Add POST /repository-agent-run/cancel endpoint with Temporal RPC error handling. Persist workflow_id upfront for cancel lifecycle ops. Expose is_cancellable metadata in flow-bridge parent workflow job responses. Co-Authored-By: Claude Opus 4.6 --- .../repo_models.py | 8 +- .../workflow_models.py | 1 + unoplat-code-confluence-commons/uv.lock | 2 +- .../src/code_confluence_flow_bridge/main.py | 32 +++- .../models/github/github_repo.py | 22 ++- .../repo_models.py | 8 +- .../api/v1/endpoints/codebase_agent_rules.py | 176 ++++++++++++++++++ .../codebase_workflow_db_activity.py | 14 +- .../repository_workflow_db_activity.py | 22 ++- .../services/temporal/cancellation_helpers.py | 29 +++ .../agent_workflow_interceptor.py | 81 +++++--- .../services/temporal/temporal_workflows.py | 18 ++ .../services/temporal/workflow_service.py | 15 +- .../workflow/workflow_run_initializer.py | 8 +- .../services/test_cancellation_helpers.py | 41 ++++ 15 files changed, 420 insertions(+), 57 deletions(-) create mode 100644 unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/cancellation_helpers.py create mode 100644 unoplat-code-confluence-query-engine/tests/services/test_cancellation_helpers.py diff --git a/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/repo_models.py b/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/repo_models.py index 8dc94fd46..b8d116769 100644 --- a/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/repo_models.py +++ b/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/repo_models.py @@ -121,7 +121,7 @@ class RepositoryWorkflowRun(SQLBase): __tablename__ = "repository_workflow_run" __table_args__ = ( CheckConstraint( - "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR')", + "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR','CANCELLED')", name="status_check", ), ForeignKeyConstraint( @@ -156,7 +156,7 @@ class RepositoryWorkflowRun(SQLBase): status: Mapped[str] = mapped_column( String, nullable=False, - comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR.", + comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED.", ) error_report: Mapped[Optional[Dict[str, Any]]] = mapped_column( JSONB, default=None, comment="Error report if the workflow run failed" @@ -200,7 +200,7 @@ class CodebaseWorkflowRun(SQLBase): __tablename__ = "codebase_workflow_run" __table_args__ = ( CheckConstraint( - "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR')", + "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR','CANCELLED')", name="codebase_status_check", ), ForeignKeyConstraint( @@ -245,7 +245,7 @@ class CodebaseWorkflowRun(SQLBase): status: Mapped[str] = mapped_column( String, nullable=False, - comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR.", + comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED.", ) error_report: Mapped[Optional[Dict[str, Any]]] = mapped_column( JSONB, default=None, comment="Error report if the workflow run failed" diff --git a/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/workflow_models.py b/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/workflow_models.py index 4ddf72d58..243785efc 100644 --- a/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/workflow_models.py +++ b/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/workflow_models.py @@ -16,6 +16,7 @@ class JobStatus(str, Enum): COMPLETED = "COMPLETED" RETRYING = "RETRYING" ERROR = "ERROR" # Partial failures (some agents succeeded, some failed) + CANCELLED = "CANCELLED" class ErrorReport(BaseModel): diff --git a/unoplat-code-confluence-commons/uv.lock b/unoplat-code-confluence-commons/uv.lock index 1be846617..2b02fc769 100644 --- a/unoplat-code-confluence-commons/uv.lock +++ b/unoplat-code-confluence-commons/uv.lock @@ -469,7 +469,7 @@ wheels = [ [[package]] name = "unoplat-code-confluence-commons" -version = "0.45.0" +version = "0.45.1" source = { editable = "." } dependencies = [ { name = "cryptography" }, diff --git a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/main.py b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/main.py index 679667b0c..1335ac9bf 100644 --- a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/main.py +++ b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/main.py @@ -35,6 +35,7 @@ Flag, ProgrammingLanguageMetadata, Repository, + RepositoryWorkflowOperation, RepositoryWorkflowRun, ) from unoplat_code_confluence_commons.credential_enums import ( @@ -146,6 +147,31 @@ ) +CANCELLABLE_PARENT_WORKFLOW_OPERATIONS: set[RepositoryWorkflowOperation] = { + RepositoryWorkflowOperation.AGENTS_GENERATION, + RepositoryWorkflowOperation.AGENT_MD_UPDATE, +} + +TERMINAL_PARENT_WORKFLOW_STATUSES: set[str] = { + JobStatus.COMPLETED.value, + JobStatus.FAILED.value, + JobStatus.TIMED_OUT.value, + JobStatus.ERROR.value, + JobStatus.CANCELLED.value, +} + + +def is_parent_workflow_cancellable( + operation: RepositoryWorkflowOperation, + status: str, +) -> bool: + """Return whether a parent workflow row can be canceled by UI users.""" + return ( + operation in CANCELLABLE_PARENT_WORKFLOW_OPERATIONS + and status not in TERMINAL_PARENT_WORKFLOW_STATUSES + ) + + # setup supertokens @@ -311,8 +337,6 @@ def create_worker( raise ApplicationError(error_message, type="WORKER_INITIALIZATION_ERROR") from e - - async def start_workflow( temporal_client: Client, repo_request: RepositoryRequestConfiguration, @@ -1490,6 +1514,10 @@ async def get_parent_workflow_jobs( started_at=run.started_at, completed_at=run.completed_at, feedback_issue_url=run.feedback_issue_url, + is_cancellable=is_parent_workflow_cancellable( + run.operation, + run.status, + ), ) for run in workflow_runs ] diff --git a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/models/github/github_repo.py b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/models/github/github_repo.py index f88aa3827..306adb109 100644 --- a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/models/github/github_repo.py +++ b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/models/github/github_repo.py @@ -132,7 +132,10 @@ class WorkflowRun(BaseModel): ) started_at: datetime = Field(description="Timestamp when the workflow run started") status: JobStatus = Field( - description="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED." + description=( + "Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, " + "TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED." + ) ) completed_at: Optional[datetime] = Field( default=None, description="Timestamp when the workflow run completed" @@ -238,7 +241,10 @@ class GithubRepoStatus(BaseModel): repository_workflow_id: str = Field(description="The ID of the repository workflow") started_at: datetime = Field(description="Timestamp when the workflow run started") status: JobStatus = Field( - description="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED." + description=( + "Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, " + "TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED." + ) ) error_report: Optional[ErrorReport] = Field( default=None, description="Error report if the workflow run failed" @@ -268,7 +274,10 @@ class ParentWorkflowJobResponse(BaseModel): description="Operation type of the workflow run. One of: INGESTION, AGENTS_GENERATION, AGENT_MD_UPDATE." ) status: JobStatus = Field( - description="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING." + description=( + "Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, " + "TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED." + ) ) started_at: datetime = Field(description="Timestamp when the workflow run started") completed_at: Optional[datetime] = Field( @@ -278,6 +287,13 @@ class ParentWorkflowJobResponse(BaseModel): default=None, description="URL of the GitHub issue created from agent feedback submission", ) + is_cancellable: bool = Field( + default=False, + description=( + "Whether this workflow run can be canceled by users from the operations " + "management UI" + ), + ) class ParentWorkflowJobListResponse(BaseModel): diff --git a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/test_data/framework_samples/unoplat_code_confluence_commons/repo_models.py b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/test_data/framework_samples/unoplat_code_confluence_commons/repo_models.py index dd11f909c..d71d0f6c2 100644 --- a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/test_data/framework_samples/unoplat_code_confluence_commons/repo_models.py +++ b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/test_data/framework_samples/unoplat_code_confluence_commons/repo_models.py @@ -118,7 +118,7 @@ class RepositoryWorkflowRun(SQLBase): __tablename__ = "repository_workflow_run" __table_args__ = ( CheckConstraint( - "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR')", + "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR','CANCELLED')", name="status_check", ), ForeignKeyConstraint( @@ -153,7 +153,7 @@ class RepositoryWorkflowRun(SQLBase): status: Mapped[str] = mapped_column( String, nullable=False, - comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR.", + comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED.", ) error_report: Mapped[Optional[Dict[str, Any]]] = mapped_column( JSONB, default=None, comment="Error report if the workflow run failed" @@ -197,7 +197,7 @@ class CodebaseWorkflowRun(SQLBase): __tablename__ = "codebase_workflow_run" __table_args__ = ( CheckConstraint( - "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR')", + "status IN ('SUBMITTED','RUNNING','FAILED','TIMED_OUT','COMPLETED','RETRYING','ERROR','CANCELLED')", name="codebase_status_check", ), ForeignKeyConstraint( @@ -242,7 +242,7 @@ class CodebaseWorkflowRun(SQLBase): status: Mapped[str] = mapped_column( String, nullable=False, - comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR.", + comment="Status of the workflow run. One of: SUBMITTED, RUNNING, FAILED, TIMED_OUT, COMPLETED, RETRYING, ERROR, CANCELLED.", ) error_report: Mapped[Optional[Dict[str, Any]]] = mapped_column( JSONB, default=None, comment="Error report if the workflow run failed" diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py index ddfa3534c..1ae0def3e 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py @@ -17,12 +17,16 @@ from loguru import logger from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import select +from temporalio.service import RPCError, RPCStatusCode from unoplat_code_confluence_commons.credential_enums import ProviderKey from unoplat_code_confluence_commons.pr_metadata_model import PrMetadata from unoplat_code_confluence_commons.repo_models import ( Repository, RepositoryAgentMdSnapshot, + RepositoryWorkflowOperation, + RepositoryWorkflowRun, ) +from unoplat_code_confluence_commons.workflow_models import JobStatus from unoplat_code_confluence_query_engine.api.deps import trace_dependency from unoplat_code_confluence_query_engine.db.postgres.ai_model_config import ( @@ -44,6 +48,7 @@ if TYPE_CHECKING: from loguru import Logger + from temporalio.client import Client router = APIRouter(prefix="/v1", tags=["codebase-rules"]) @@ -55,6 +60,14 @@ class RepositoryWorkflowRunResponse(BaseModel): trace_id: str +class RepositoryAgentRunCancelResponse(BaseModel): + """Response returned when a repository agent workflow cancel is requested.""" + + repository_workflow_run_id: str + status: Literal["cancel_requested"] + message: str + + class RepositoryAgentMdPrRequest(BaseModel): """Request payload to manually create/update AGENTS.md PR.""" @@ -83,6 +96,35 @@ class RepositoryAgentMdPrStatusResponse(BaseModel): pr_metadata: RepositoryAgentMdPrResponse | None = None +TERMINAL_REPOSITORY_WORKFLOW_STATUSES: set[str] = { + JobStatus.COMPLETED.value, + JobStatus.FAILED.value, + JobStatus.TIMED_OUT.value, + JobStatus.ERROR.value, + JobStatus.CANCELLED.value, +} + +CANCELLABLE_REPOSITORY_WORKFLOW_OPERATIONS: set[RepositoryWorkflowOperation] = { + RepositoryWorkflowOperation.AGENTS_GENERATION, + RepositoryWorkflowOperation.AGENT_MD_UPDATE, +} + + +def _is_terminal_repository_workflow_status(status: str) -> bool: + return status in TERMINAL_REPOSITORY_WORKFLOW_STATUSES + + +async def _cancel_temporal_workflow( + *, + temporal_client: Client, + workflow_id: str, +) -> None: + workflow_handle = temporal_client.get_workflow_handle( # pyright: ignore[reportUnknownMemberType] + workflow_id + ) + await workflow_handle.cancel() + + def _get_mapping_field( payload: Mapping[str, object], key: str, @@ -415,6 +457,140 @@ async def start_repository_agent_run( ) +@router.post("/repository-agent-run/cancel") +async def cancel_repository_agent_run( + owner_name: str = Query(..., description="Repository owner name"), + repo_name: str = Query(..., description="Repository name"), + repository_workflow_run_id: str = Query( + ..., description="Repository workflow run ID" + ), + bound_logger: "Logger" = Depends(trace_dependency), +) -> RepositoryAgentRunCancelResponse: + """Cancel an in-flight repository agent workflow. + + This endpoint supports cancellation only for agent workflows + (AGENTS_GENERATION, AGENT_MD_UPDATE). INGESTION workflows are explicitly + non-cancellable from this API. + """ + worker_manager = get_worker_manager() + + if not worker_manager.is_running: + bound_logger.error( + "[codebase_agent_rules] Temporal worker not running for cancel request" + ) + raise HTTPException( + status_code=503, + detail=( + "Agent runtime is unavailable. Please verify model/tool configuration " + "and retry." + ), + ) + + async with get_startup_session() as session: + workflow_run = await session.get( + RepositoryWorkflowRun, + (repo_name, owner_name, repository_workflow_run_id), + ) + + if workflow_run is None: + raise HTTPException( + status_code=404, + detail=( + "Repository workflow run not found for " + f"{owner_name}/{repo_name} run_id={repository_workflow_run_id}" + ), + ) + + if workflow_run.operation not in CANCELLABLE_REPOSITORY_WORKFLOW_OPERATIONS: + raise HTTPException( + status_code=400, + detail=( + "Cancellation is only supported for AGENTS_GENERATION and " + "AGENT_MD_UPDATE workflows" + ), + ) + + if _is_terminal_repository_workflow_status(workflow_run.status): + raise HTTPException( + status_code=409, + detail=( + f"Workflow run is already in terminal state ({workflow_run.status})" + ), + ) + + workflow_id = workflow_run.repository_workflow_id + + try: + await _cancel_temporal_workflow( + temporal_client=worker_manager.client, + workflow_id=workflow_id, + ) + except RPCError as rpc_error: + if rpc_error.status == RPCStatusCode.NOT_FOUND: + raise HTTPException( + status_code=404, + detail=( + "Temporal workflow not found for " + f"workflow_id={workflow_id}; run may have already finished" + ), + ) from rpc_error + + if rpc_error.status in ( + RPCStatusCode.FAILED_PRECONDITION, + RPCStatusCode.ABORTED, + ): + raise HTTPException( + status_code=409, + detail=( + "Workflow run is no longer cancellable because it is already " + "finishing or finished" + ), + ) from rpc_error + + bound_logger.error( + "[codebase_agent_rules] Cancel failed for {}/{} run_id={} workflow_id={} status={} message={}", + owner_name, + repo_name, + repository_workflow_run_id, + workflow_id, + rpc_error.status, + rpc_error.message, + ) + raise HTTPException( + status_code=500, + detail="Failed to cancel repository agent workflow", + ) from rpc_error + except Exception as cancel_error: + bound_logger.error( + "[codebase_agent_rules] Unexpected cancel error for {}/{} run_id={} workflow_id={}: {}", + owner_name, + repo_name, + repository_workflow_run_id, + workflow_id, + cancel_error, + ) + raise HTTPException( + status_code=500, + detail="Failed to cancel repository agent workflow", + ) from cancel_error + + bound_logger.info( + "[codebase_agent_rules] Cancel requested for {}/{} run_id={} workflow_id={}", + owner_name, + repo_name, + repository_workflow_run_id, + workflow_id, + ) + return RepositoryAgentRunCancelResponse( + repository_workflow_run_id=repository_workflow_run_id, + status="cancel_requested", + message=( + "Cancel requested successfully. The workflow may take a short time " + "to reach a terminal state." + ), + ) + + @router.get("/repository-agent-snapshot") async def get_repository_agent_snapshot( owner_name: str = Query(..., description="Repository owner name"), diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/codebase_workflow_db_activity.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/codebase_workflow_db_activity.py index cf298f15a..0d6a6c31f 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/codebase_workflow_db_activity.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/codebase_workflow_db_activity.py @@ -82,10 +82,15 @@ async def update_codebase_workflow_status( else: # UPDATE existing record # Status preservation: FAILED/ERROR status should never be overwritten with COMPLETED - if existing.status in ( - JobStatus.FAILED.value, - JobStatus.ERROR.value, - ) and envelope.status == JobStatus.COMPLETED.value: + if ( + existing.status + in ( + JobStatus.FAILED.value, + JobStatus.ERROR.value, + JobStatus.CANCELLED.value, + ) + and envelope.status == JobStatus.COMPLETED.value + ): logger.warning( "[codebase_workflow_db_activity] Preserving {} status - " "ignoring COMPLETED update for {}/{}/{} run_id={}", @@ -110,6 +115,7 @@ async def update_codebase_workflow_status( JobStatus.FAILED.value, JobStatus.TIMED_OUT.value, JobStatus.ERROR.value, + JobStatus.CANCELLED.value, ): existing.completed_at = now diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/repository_workflow_db_activity.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/repository_workflow_db_activity.py index 83c10d22b..d08dab57d 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/repository_workflow_db_activity.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/activities/repository_workflow_db_activity.py @@ -55,6 +55,12 @@ async def update_repository_workflow_status( existing = result.scalar_one_or_none() now = datetime.now(timezone.utc) + workflow_id = envelope.workflow_id + + if workflow_id is None: + raise ValueError( + "ParentWorkflowDbActivityEnvelope.workflow_id is required" + ) if existing is None: # CREATE new record @@ -65,7 +71,7 @@ async def update_repository_workflow_status( repository_name=envelope.repository_name, repository_owner_name=envelope.repository_owner_name, repository_workflow_run_id=envelope.workflow_run_id, - repository_workflow_id=envelope.workflow_id, + repository_workflow_id=workflow_id, operation=envelope.operation, status=envelope.status, started_at=now, @@ -84,6 +90,7 @@ async def update_repository_workflow_status( in ( JobStatus.FAILED.value, JobStatus.ERROR.value, + JobStatus.CANCELLED.value, ) and envelope.status == JobStatus.COMPLETED.value ): @@ -102,6 +109,18 @@ async def update_repository_workflow_status( existing.status, envelope.status, ) + + if existing.repository_workflow_id != workflow_id: + logger.warning( + "[repository_workflow_db_activity] Correcting repository_workflow_id for {}/{} run_id={} from {} to {}", + envelope.repository_owner_name, + envelope.repository_name, + envelope.workflow_run_id, + existing.repository_workflow_id, + workflow_id, + ) + existing.repository_workflow_id = workflow_id + existing.status = envelope.status # Set completed_at for terminal states @@ -110,6 +129,7 @@ async def update_repository_workflow_status( JobStatus.FAILED.value, JobStatus.TIMED_OUT.value, JobStatus.ERROR.value, + JobStatus.CANCELLED.value, ): existing.completed_at = now diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/cancellation_helpers.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/cancellation_helpers.py new file mode 100644 index 000000000..e37ebd58f --- /dev/null +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/cancellation_helpers.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import asyncio + +from temporalio.exceptions import CancelledError, FailureError + +MAX_EXCEPTION_CAUSE_DEPTH = 8 + + +def is_temporal_cancellation_exception(exception: BaseException) -> bool: + """Return whether an exception represents Temporal workflow cancellation.""" + current_exception = exception + + for _ in range(MAX_EXCEPTION_CAUSE_DEPTH): + if isinstance(current_exception, (CancelledError, asyncio.CancelledError)): + return True + + next_exception: BaseException | None = None + if isinstance(current_exception, FailureError) and current_exception.cause: + next_exception = current_exception.cause + elif isinstance(current_exception.__cause__, BaseException): + next_exception = current_exception.__cause__ + + if next_exception is None or next_exception is current_exception: + return False + + current_exception = next_exception + + return False diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/interceptors/agent_workflow_interceptor.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/interceptors/agent_workflow_interceptor.py index 161e8e945..43645cedb 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/interceptors/agent_workflow_interceptor.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/interceptors/agent_workflow_interceptor.py @@ -10,6 +10,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta import traceback from typing import Any, Optional @@ -18,7 +19,11 @@ from temporalio import workflow from temporalio.api.common.v1 import Payload from temporalio.common import RetryPolicy -from temporalio.exceptions import ActivityError, ApplicationError, ChildWorkflowError +from temporalio.exceptions import ( + ActivityError, + ApplicationError, + ChildWorkflowError, +) from temporalio.worker._interceptor import ( ExecuteWorkflowInput, Interceptor, @@ -26,6 +31,10 @@ ) from temporalio.workflow import Info +from unoplat_code_confluence_query_engine.services.temporal.cancellation_helpers import ( + is_temporal_cancellation_exception, +) + with workflow.unsafe.imports_passed_through(): from loguru import logger from unoplat_code_confluence_commons.workflow_envelopes import ( @@ -61,6 +70,11 @@ maximum_interval=timedelta(seconds=10), ) +WORKFLOW_EXECUTION_EXCEPTIONS: tuple[type[BaseException], ...] = ( + asyncio.CancelledError, + Exception, +) + class AgentWorkflowStatusInterceptor(Interceptor): """Worker interceptor factory for agent workflow status tracking. @@ -230,14 +244,21 @@ async def _handle_repository_workflow( bound_logger.info( "[agent_workflow_interceptor] RepositoryAgentWorkflow completed successfully" ) - except (ActivityError, ChildWorkflowError, ApplicationError, Exception) as e: + except WORKFLOW_EXECUTION_EXCEPTIONS as e: exc = e - status = JobStatus.ERROR.value - error_report = self._build_error_report(e) - bound_logger.error( - "[agent_workflow_interceptor] RepositoryAgentWorkflow failed: {}", - str(e), - ) + if is_temporal_cancellation_exception(e): + status = JobStatus.CANCELLED.value + error_report = None + bound_logger.info( + "[agent_workflow_interceptor] RepositoryAgentWorkflow cancelled" + ) + else: + status = JobStatus.ERROR.value + error_report = self._build_error_report(e) + bound_logger.error( + "[agent_workflow_interceptor] RepositoryAgentWorkflow failed: {}", + str(e), + ) finally: # Record final status final_envelope = ParentWorkflowDbActivityEnvelope( @@ -362,14 +383,21 @@ async def _handle_codebase_workflow( bound_logger.info( "[agent_workflow_interceptor] CodebaseAgentWorkflow completed successfully" ) - except (ActivityError, ChildWorkflowError, ApplicationError, Exception) as e: + except WORKFLOW_EXECUTION_EXCEPTIONS as e: exc = e - status = JobStatus.ERROR.value - error_report = self._build_error_report(e) - bound_logger.error( - "[agent_workflow_interceptor] CodebaseAgentWorkflow failed: {}", - str(e), - ) + if is_temporal_cancellation_exception(e): + status = JobStatus.CANCELLED.value + error_report = None + bound_logger.info( + "[agent_workflow_interceptor] CodebaseAgentWorkflow cancelled" + ) + else: + status = JobStatus.ERROR.value + error_report = self._build_error_report(e) + bound_logger.error( + "[agent_workflow_interceptor] CodebaseAgentWorkflow failed: {}", + str(e), + ) finally: # Record final status for codebase workflow final_envelope = CodebaseWorkflowDbActivityEnvelope( @@ -438,11 +466,11 @@ def _build_error_report(self, exc: BaseException) -> ErrorReport: ErrorReport with error details """ # Get root cause for activity/child workflow errors - root = exc - if isinstance(exc, (ActivityError, ChildWorkflowError)) and getattr( - exc, "cause", None - ): - root = exc.cause # type: ignore[assignment] + root: BaseException = exc + if isinstance(exc, ActivityError) and exc.cause: + root = exc.cause + elif isinstance(exc, ChildWorkflowError) and exc.cause: + root = exc.cause # Build metadata - always start with dict (AC#4: merge model error details) metadata: dict[str, Any] = {} @@ -461,15 +489,14 @@ def _build_error_report(self, exc: BaseException) -> ErrorReport: } # Extract and MERGE model error details using shared utility (AC#4) - if root is not None: - model_error_details = extract_model_error_from_exception(root) - if not model_error_details and isinstance(root, ApplicationError): - model_error_details = extract_model_error_from_details(root.details) - if model_error_details: - metadata["model_error"] = model_error_details + model_error_details = extract_model_error_from_exception(root) + if not model_error_details and isinstance(root, ApplicationError): + model_error_details = extract_model_error_from_details(root.details) + if model_error_details: + metadata["model_error"] = model_error_details return ErrorReport( error_message=str(root), - stack_trace=traceback.format_exc() if root is not None else "", + stack_trace=traceback.format_exc(), metadata=metadata if metadata else None, ) diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/temporal_workflows.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/temporal_workflows.py index 01b81f4ef..53a3421f0 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/temporal_workflows.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/temporal_workflows.py @@ -58,6 +58,9 @@ from unoplat_code_confluence_query_engine.services.temporal.activities.repository_agent_snapshot_activity import ( RepositoryAgentSnapshotActivity, ) + from unoplat_code_confluence_query_engine.services.temporal.cancellation_helpers import ( + is_temporal_cancellation_exception, + ) from unoplat_code_confluence_query_engine.services.temporal.interceptors.agent_workflow_interceptor import ( DB_ACTIVITY_RETRY_POLICY, ) @@ -112,6 +115,13 @@ def _enrich_agent_error_with_model_details( return error_dict +def _raise_if_temporal_cancellation(exception: BaseException) -> None: + """Re-raise cancellation-shaped exceptions so workflow cancel is preserved.""" + if is_temporal_cancellation_exception(exception): + logger.info("[workflow] Cancellation detected, re-raising") + raise exception + + def _build_dependency_guide_prompt( dependency_target: DependencyGuideTarget, programming_language: str, @@ -190,6 +200,7 @@ async def _run_section_updater( codebase_metadata.codebase_name, ) except Exception as e: + _raise_if_temporal_cancellation(e) logger.error( "[workflow] {} failed for {}: {}", updater_agent_name, @@ -321,6 +332,7 @@ async def _run_call_expression_validation( ) agent_stats.append(extract_usage_statistics(validator_result.usage())) except Exception as validator_error: + _raise_if_temporal_cancellation(validator_error) logger.error( "[workflow] call_expression_validator failed for {}:{}:{}:{}:{}-{}: {}", candidate.identity.feature_language, @@ -482,6 +494,7 @@ async def run( updater_runs=results["agents_md_updater_runs"], ) except Exception as e: + _raise_if_temporal_cancellation(e) logger.error( "[workflow] development_workflow_guide failed for {}: {}", codebase_metadata.codebase_name, @@ -559,6 +572,7 @@ async def run( extract_usage_statistics(result.usage()) ) except Exception as dep_error: + _raise_if_temporal_cancellation(dep_error) logger.warning( "[workflow] Failed to document dependency '{}': {}", dependency_target.name, @@ -613,6 +627,7 @@ async def run( updater_runs=results["agents_md_updater_runs"], ) except Exception as e: + _raise_if_temporal_cancellation(e) logger.error( "[workflow] dependency_guide failed for {}: {}", codebase_metadata.codebase_name, @@ -697,6 +712,7 @@ async def run( updater_runs=results["agents_md_updater_runs"], ) except Exception as e: + _raise_if_temporal_cancellation(e) logger.error( "[workflow] business_domain_guide failed for {}: {}", codebase_metadata.codebase_name, @@ -799,6 +815,7 @@ async def run( updater_runs=results["agents_md_updater_runs"], ) except Exception as e: + _raise_if_temporal_cancellation(e) logger.error( "[workflow] app_interfaces_agent failed for {}: {}", codebase_metadata.codebase_name, @@ -938,6 +955,7 @@ async def run( for (codebase_name, _), result in zip(child_handles, results_list): if isinstance(result, BaseException): + _raise_if_temporal_cancellation(result) logger.error( "[workflow] CodebaseAgentWorkflow failed for {}/{}: {}", repository_qualified_name, diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/workflow_service.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/workflow_service.py index 923c0258e..50477974c 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/workflow_service.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/workflow_service.py @@ -90,12 +90,17 @@ async def start_workflow( repository_workflow_run_id, ) + # Generate Temporal workflow ID up front so it can be persisted with the + # repository workflow run row for later lifecycle operations (cancel, describe). + workflow_id = f"agent-{repository_qualified_name.replace('/', '-')}-{uuid.uuid4().hex[:8]}" + # Ensure parent records exist (Repository + RepositoryWorkflowRun) # This satisfies foreign key constraints for RepositoryAgentMdSnapshot await ensure_workflow_run_exists( owner_name=owner_name, repo_name=repo_name, repository_workflow_run_id=repository_workflow_run_id, + repository_workflow_id=workflow_id, ) bound_logger.info( "[workflow_service] Ensured Repository and RepositoryWorkflowRun records exist" @@ -123,9 +128,6 @@ async def start_workflow( metadata.model_dump() for metadata in ruleset_metadata.codebase_metadata ] - # Generate workflow ID - workflow_id = f"agent-{repository_qualified_name.replace('/', '-')}-{uuid.uuid4().hex[:8]}" - bound_logger.info( "[workflow_service] Starting RepositoryAgentWorkflow with id={}, " "repository={}, codebases={}, trace_id={}", @@ -136,7 +138,7 @@ async def start_workflow( ) # Start the workflow (non-blocking - returns immediately) - await self._client.start_workflow( + workflow_handle = await self._client.start_workflow( RepositoryAgentWorkflow.run, args=[ repository_qualified_name, @@ -149,8 +151,9 @@ async def start_workflow( ) bound_logger.info( - "[workflow_service] Workflow started successfully: workflow_id={}, run_id={}", - workflow_id, + "[workflow_service] Workflow started successfully: workflow_id={}, temporal_run_id={}, run_id={}", + workflow_handle.id, + workflow_handle.result_run_id, repository_workflow_run_id, ) diff --git a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/workflow/workflow_run_initializer.py b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/workflow/workflow_run_initializer.py index 606467817..1412a46e2 100644 --- a/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/workflow/workflow_run_initializer.py +++ b/unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/workflow/workflow_run_initializer.py @@ -22,7 +22,7 @@ async def ensure_workflow_run_exists( owner_name: str, repo_name: str, repository_workflow_run_id: str, - repository_workflow_id: str | None = None, + repository_workflow_id: str, operation: RepositoryWorkflowOperation = RepositoryWorkflowOperation.AGENTS_GENERATION, provider_key: ProviderKey = ProviderKey.GITHUB_OPEN, ) -> None: @@ -35,12 +35,10 @@ async def ensure_workflow_run_exists( owner_name: Repository owner name (e.g., "unoplat") repo_name: Repository name (e.g., "unoplat-code-confluence") repository_workflow_run_id: Unique workflow run identifier - repository_workflow_id: Optional workflow identifier (defaults to run_id) + repository_workflow_id: Temporal workflow identifier operation: Type of workflow operation being performed provider_key: Git provider for the repository """ - workflow_id = repository_workflow_id or repository_workflow_run_id - async with get_startup_session() as session: # Check if Repository exists, create if not repository = await session.get(Repository, (repo_name, owner_name)) @@ -68,7 +66,7 @@ async def ensure_workflow_run_exists( repository_name=repo_name, repository_owner_name=owner_name, repository_workflow_run_id=repository_workflow_run_id, - repository_workflow_id=workflow_id, + repository_workflow_id=repository_workflow_id, operation=operation, status="RUNNING", started_at=now, diff --git a/unoplat-code-confluence-query-engine/tests/services/test_cancellation_helpers.py b/unoplat-code-confluence-query-engine/tests/services/test_cancellation_helpers.py new file mode 100644 index 000000000..ad5df3a43 --- /dev/null +++ b/unoplat-code-confluence-query-engine/tests/services/test_cancellation_helpers.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import asyncio + +from temporalio.exceptions import CancelledError + +from unoplat_code_confluence_query_engine.services.temporal.cancellation_helpers import ( + is_temporal_cancellation_exception, +) + + +def build_wrapped_temporal_cancelled_exception() -> RuntimeError: + try: + raise CancelledError() + except CancelledError as cancelled_error: + try: + raise ValueError("inner") from cancelled_error + except ValueError as inner_error: + try: + raise RuntimeError("outer") from inner_error + except RuntimeError as wrapped_error: + return wrapped_error + + raise AssertionError("Expected wrapped cancellation exception") + + +def test_detects_direct_temporal_cancelled_error() -> None: + assert is_temporal_cancellation_exception(CancelledError()) is True + + +def test_detects_direct_asyncio_cancelled_error() -> None: + assert is_temporal_cancellation_exception(asyncio.CancelledError()) is True + + +def test_detects_wrapped_temporal_cancelled_error() -> None: + wrapped_error = build_wrapped_temporal_cancelled_exception() + assert is_temporal_cancellation_exception(wrapped_error) is True + + +def test_ignores_non_cancelled_exception() -> None: + assert is_temporal_cancellation_exception(RuntimeError("boom")) is False From 1db5b58aa179a52d56deb9184f58257664f65a25 Mon Sep 17 00:00:00 2001 From: JayGhiya Date: Thu, 12 Mar 2026 16:34:56 +0530 Subject: [PATCH 2/4] feat(frontend): add cancel workflow action and CANCELLED status to operations UI Add cancelRepositoryAgentRun API client and CANCELLED to JobStatus union. Implement cancel button with mutation in GenerateAgentsDialog, gated by is_cancellable flag and operation type. Refactor dialog to tabbed progress/statistics layout. Add ERROR and CANCELLED status icons, badge variants, and filter options to job status and submitted jobs data tables. Add .task/ to .gitignore. Co-Authored-By: Claude Opus 4.6 --- unoplat-code-confluence-frontend/.gitignore | 3 +- .../custom/GenerateAgentsDialog.tsx | 353 +++++++++++++----- .../custom/job-status-data-table-columns.tsx | 14 +- .../submitted-jobs-data-table-columns.tsx | 6 + .../src/lib/api.ts | 29 ++ unoplat-code-confluence-frontend/src/types.ts | 5 +- 6 files changed, 321 insertions(+), 89 deletions(-) diff --git a/unoplat-code-confluence-frontend/.gitignore b/unoplat-code-confluence-frontend/.gitignore index c5921d757..45ffdeb7d 100644 --- a/unoplat-code-confluence-frontend/.gitignore +++ b/unoplat-code-confluence-frontend/.gitignore @@ -74,4 +74,5 @@ node_modules/ # Task files # tasks.json -# tasks/ +# tasks/ +.task/ diff --git a/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx b/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx index 0bd515c8d..d9b308f67 100644 --- a/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx +++ b/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx @@ -13,6 +13,8 @@ import type { CodebaseStatus, WorkflowStatus, WorkflowRun, + JobStatus, + RepositoryWorkflowOperation, } from "@/types"; import { Dialog, @@ -34,6 +36,7 @@ import { } from "@/features/repository-agent-snapshots/hooks"; import type { RepositoryAgentMdPrStatusResponse } from "@/lib/api"; import { + cancelRepositoryAgentRun, createRepositoryAgentMdPr, getParentWorkflowJobs, getRepositoryAgentMdPrStatus, @@ -43,7 +46,9 @@ import { apiToUiErrorReport } from "@/lib/error-utils"; import { FeedbackDialog } from "@/components/custom/FeedbackDialog"; import { AgentFeedbackSheet } from "@/features/agent-feedback"; import { ButtonGroup } from "@/components/ui/button-group"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { XCircle, ExternalLink, MessageSquare } from "lucide-react"; +import { toast } from "sonner"; // Data structure for aggregated errors (repository + codebase errors combined) interface AggregatedErrorData { @@ -64,7 +69,7 @@ interface GenerateAgentsDialogProps { job: ParentWorkflowJobResponse | null; } -function getUnknownErrorMessage(error: unknown): string { +function getApiErrorMessage(error: unknown, fallbackMessage: string): string { if ( typeof error === "object" && error !== null && @@ -76,7 +81,19 @@ function getUnknownErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; } - return "Failed to publish PR"; + return fallbackMessage; +} + +function isInFlightJobStatus(status: JobStatus): boolean { + return ( + status === "SUBMITTED" || status === "RUNNING" || status === "RETRYING" + ); +} + +function isAgentWorkflowOperation( + operation: RepositoryWorkflowOperation, +): boolean { + return operation === "AGENTS_GENERATION" || operation === "AGENT_MD_UPDATE"; } /** @@ -103,6 +120,37 @@ export function GenerateAgentsDialog({ React.useState(false); const queryClient = useQueryClient(); + const cancelRunMutation = useMutation({ + mutationFn: (payload: { + ownerName: string; + repoName: string; + repositoryWorkflowRunId: string; + }) => + cancelRepositoryAgentRun( + payload.ownerName, + payload.repoName, + payload.repositoryWorkflowRunId, + ), + onSuccess: (response, variables) => { + toast.success(response.message); + queryClient.invalidateQueries({ queryKey: ["parentWorkflowJobs"] }); + queryClient.invalidateQueries({ + queryKey: [ + "repositoryStatus", + variables.repoName, + variables.ownerName, + variables.repositoryWorkflowRunId, + ], + }); + handleDialogOpenChange(false); + }, + onError: (error: unknown) => { + toast.error( + getApiErrorMessage(error, "Failed to cancel AGENTS.md workflow"), + ); + }, + }); + const createPrMutation = useMutation({ mutationFn: createRepositoryAgentMdPr, onSuccess: () => { @@ -206,10 +254,16 @@ export function GenerateAgentsDialog({ // Derive status from actualJob (fresh cache data or prop fallback) const jobStatus = actualJob?.status ?? "SUBMITTED"; - const isRunning = jobStatus === "RUNNING" || jobStatus === "SUBMITTED"; + const isRunning = isInFlightJobStatus(jobStatus); const isCompleted = jobStatus === "COMPLETED"; // ERROR = partial failure (some agents succeeded, some failed) const isFailed = jobStatus === "FAILED" || jobStatus === "ERROR"; + const isCancelled = jobStatus === "CANCELLED"; + const canCancelWorkflow = + !!actualJob && + isAgentWorkflowOperation(actualJob.operation) && + isRunning && + (actualJob.is_cancellable ?? true); // Persisted PR status — query when dialog open + job completed const { data: persistedPrStatus } = @@ -419,16 +473,29 @@ export function GenerateAgentsDialog({ }); }; + const handleCancelWorkflow = (): void => { + if (!actualJob || cancelRunMutation.isPending || !canCancelWorkflow) { + return; + } + + cancelRunMutation.mutate({ + ownerName: actualJob.repository_owner_name, + repoName: actualJob.repository_name, + repositoryWorkflowRunId: actualJob.repository_workflow_run_id, + }); + }; + const handleDialogOpenChange = (nextOpen: boolean): void => { if (!nextOpen) { createPrMutation.reset(); + cancelRunMutation.reset(); } onOpenChange(nextOpen); }; const prResult = createPrMutation.data ?? persistedPrResult; const prErrorMessage = createPrMutation.isError - ? getUnknownErrorMessage(createPrMutation.error) + ? getApiErrorMessage(createPrMutation.error, "Failed to publish PR") : null; if (!job) { @@ -448,7 +515,7 @@ export function GenerateAgentsDialog({ {/* Section: Run Details */} -
+
Run Details
@@ -471,97 +538,186 @@ export function GenerateAgentsDialog({
- {/* Section: Progress */} -
- Progress -
- - {/* Main Content Area */} -
- {/* Loading State */} - {isLiveLoading && !isLiveReady && ( -
Connecting to real-time updates...
- )} + {/* Main Content Area — tabbed when statistics available, flat otherwise */} + {showStatistics && parsedSnapshot?.statistics ? ( + + + Progress + Statistics + + + + {/* Loading State */} + {isLiveLoading && !isLiveReady && ( +
+ Connecting to real-time updates... +
+ )} - {/* Error State */} - {isAnyLiveError && ( -
-
- Electric sync encountered an error. -
-
- -
- Status: {isProgressError ? "progress error" : liveStatus} + {/* Error State */} + {isAnyLiveError && ( +
+
+ Electric sync encountered an error. +
+
+ +
+ Status:{" "} + {isProgressError ? "progress error" : liveStatus} +
+
-
-
- )} + )} - {/* Progress Display - shown when we have codebaseIds */} - {codebaseIds.length > 0 && scope && ( - - )} + {/* Progress Display - shown when we have codebaseIds */} + {codebaseIds.length > 0 && scope && ( + + )} - {/* Waiting for data state */} - {!isAnyLiveError && codebaseIds.length === 0 && isRunning && ( -
-

Waiting for workflow to start...

-

- Real-time updates will appear automatically. -

-
- )} + {/* Waiting for data state */} + {!isAnyLiveError && codebaseIds.length === 0 && isRunning && ( +
+

Waiting for workflow to start...

+

+ Real-time updates will appear automatically. +

+
+ )} + - {/* Statistics Display (for completed jobs) */} - {showStatistics && parsedSnapshot?.statistics && ( - <> - {/* Section: Statistics */} -
- Statistics -
+ - - )} -
+
+
+ ) : ( + <> + {/* Section: Progress (no tabs when statistics unavailable) */} +
+ Progress +
+ +
+ {/* Loading State */} + {isLiveLoading && !isLiveReady && ( +
+ Connecting to real-time updates... +
+ )} + + {/* Error State */} + {isAnyLiveError && ( +
+
+ Electric sync encountered an error. +
+
+ +
+ Status:{" "} + {isProgressError ? "progress error" : liveStatus} +
+
+
+ )} + + {/* Progress Display - shown when we have codebaseIds */} + {codebaseIds.length > 0 && scope && ( + + )} + + {/* Waiting for data state */} + {!isAnyLiveError && codebaseIds.length === 0 && isRunning && ( +
+

Waiting for workflow to start...

+

+ Real-time updates will appear automatically. +

+
+ )} +
+ + )} {/* Footer Actions */} -
+
{/* Left side: Status + Actions based on job state */} {isFailed ? ( @@ -655,6 +811,31 @@ export function GenerateAgentsDialog({
)}
+ ) : isRunning ? ( + + + In Progress + + {canCancelWorkflow && ( + + )} + + ) : isCancelled ? ( + + + + Cancelled + + ) : (
)} diff --git a/unoplat-code-confluence-frontend/src/components/custom/job-status-data-table-columns.tsx b/unoplat-code-confluence-frontend/src/components/custom/job-status-data-table-columns.tsx index 462f88b26..c0eb32f00 100644 --- a/unoplat-code-confluence-frontend/src/components/custom/job-status-data-table-columns.tsx +++ b/unoplat-code-confluence-frontend/src/components/custom/job-status-data-table-columns.tsx @@ -6,10 +6,12 @@ import { Button } from "../ui/button"; import { Badge } from "../ui/badge"; import { AlertCircle, + AlertTriangle, CheckCircle, Clock, PauseCircle, RefreshCw, + XCircle, } from "lucide-react"; // Reusing the status icon and styles functions from the parent component @@ -27,6 +29,10 @@ export const getStatusIcon = (status: JobStatus): React.ReactNode => { return ; case "RETRYING": return ; + case "ERROR": + return ; + case "CANCELLED": + return ; default: return ; } @@ -34,7 +40,7 @@ export const getStatusIcon = (status: JobStatus): React.ReactNode => { export const getStatusVariant = ( status: JobStatus, -): "completed" | "failed" | "pending" | "running" | "cancelled" => { +): "completed" | "failed" | "pending" | "running" | "cancelled" | "error" => { switch (status) { case "COMPLETED": return "completed"; @@ -46,6 +52,10 @@ export const getStatusVariant = ( case "RUNNING": case "RETRYING": return "running"; + case "ERROR": + return "error"; + case "CANCELLED": + return "cancelled"; default: return "cancelled"; } @@ -124,6 +134,8 @@ export function getJobStatusDataTableColumns({ { label: "Failed", value: "FAILED" }, { label: "Timed Out", value: "TIMED_OUT" }, { label: "Retrying", value: "RETRYING" }, + { label: "Error", value: "ERROR" }, + { label: "Cancelled", value: "CANCELLED" }, ], }, enableColumnFilter: true, diff --git a/unoplat-code-confluence-frontend/src/components/custom/submitted-jobs-data-table-columns.tsx b/unoplat-code-confluence-frontend/src/components/custom/submitted-jobs-data-table-columns.tsx index 20c6fb93e..04c72fc90 100644 --- a/unoplat-code-confluence-frontend/src/components/custom/submitted-jobs-data-table-columns.tsx +++ b/unoplat-code-confluence-frontend/src/components/custom/submitted-jobs-data-table-columns.tsx @@ -24,6 +24,7 @@ import { RefreshCw, Cpu, FileCode, + XCircle, } from "lucide-react"; export const getStatusIcon = (status: JobStatus): React.ReactNode => { @@ -42,6 +43,8 @@ export const getStatusIcon = (status: JobStatus): React.ReactNode => { return ; case "ERROR": return ; + case "CANCELLED": + return ; default: return ; } @@ -63,6 +66,8 @@ export const getStatusVariant = ( return "running"; case "ERROR": return "error"; + case "CANCELLED": + return "cancelled"; default: return "cancelled"; } @@ -216,6 +221,7 @@ export const submittedJobsColumns: ColumnDef[] = [ { label: "Timed Out", value: "TIMED_OUT" }, { label: "Retrying", value: "RETRYING" }, { label: "Error", value: "ERROR" }, + { label: "Cancelled", value: "CANCELLED" }, ], }, enableColumnFilter: true, diff --git a/unoplat-code-confluence-frontend/src/lib/api.ts b/unoplat-code-confluence-frontend/src/lib/api.ts index 09cbe8f17..7e1c4db0d 100644 --- a/unoplat-code-confluence-frontend/src/lib/api.ts +++ b/unoplat-code-confluence-frontend/src/lib/api.ts @@ -621,6 +621,12 @@ export interface RepositoryWorkflowRunResponse { repository_workflow_run_id: string; } +export interface RepositoryAgentRunCancelResponse { + repository_workflow_run_id: string; + status: "cancel_requested"; + message: string; +} + export interface RepositoryAgentMdPrRequest { owner_name: string; repo_name: string; @@ -662,6 +668,29 @@ export async function startRepositoryAgentRun( } } +export async function cancelRepositoryAgentRun( + ownerName: string, + repoName: string, + repositoryWorkflowRunId: string, +): Promise { + try { + const params = { + owner_name: ownerName, + repo_name: repoName, + repository_workflow_run_id: repositoryWorkflowRunId, + }; + + const response: AxiosResponse = + await queryEngineClient.post("/v1/repository-agent-run/cancel", null, { + params, + }); + + return response.data; + } catch (error: unknown) { + throw handleApiError(error); + } +} + export async function createRepositoryAgentMdPr( payload: RepositoryAgentMdPrRequest, ): Promise { diff --git a/unoplat-code-confluence-frontend/src/types.ts b/unoplat-code-confluence-frontend/src/types.ts index a2aa2cf1a..06f916747 100644 --- a/unoplat-code-confluence-frontend/src/types.ts +++ b/unoplat-code-confluence-frontend/src/types.ts @@ -155,7 +155,8 @@ export type JobStatus = | "TIMED_OUT" | "COMPLETED" | "RETRYING" - | "ERROR"; // Partial failures (some agents succeeded, some failed) + | "ERROR" // Partial failures (some agents succeeded, some failed) + | "CANCELLED"; // Repository workflow operation type export type RepositoryWorkflowOperation = @@ -174,6 +175,8 @@ export interface ParentWorkflowJobResponse { completed_at?: string | null; /** GitHub issue URL for user feedback on agent generation (if submitted) */ feedback_issue_url?: string | null; + /** Whether this run can be canceled from operations management */ + is_cancellable?: boolean; } // Parent workflow jobs list response From 2a0f7eaae627ca45e1d0c7f1715cf62b2b7b7fbb Mon Sep 17 00:00:00 2001 From: JayGhiya Date: Thu, 12 Mar 2026 16:40:20 +0530 Subject: [PATCH 3/4] chore(docs): update task statuses and add cancellation task definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add task-12 (cancel workflow action) and task-12.1 (prevent duplicate runs) - Mark completed tasks as Done (task-1.x, task-3.x, task-4.1, task-6–10.x) - Delete superseded task-6 (Dockerfile fix) - Update app_interfaces.md for cancel endpoint and cancellability metadata Co-Authored-By: Claude Opus 4.6 --- ...unctionDefinition-export-name-filtering.md | 4 +- ...idator-agent-into-query-engine-workflow.md | 4 +- ...n-candidate-hydration-and-mapper-gating.md | 4 +- ...calable-history-and-virtualized-viewing.md | 5 +- ...story-for-Electric-backed-frontend-sync.md | 5 +- ...n-body-with-a-virtualized-flat-timeline.md | 5 +- ...-collection-and-merge-with-live-updates.md | 5 +- ...gent-workflows-in-Operations-Management.md | 154 ++++++++++++++++++ ...GENTS.md-run-starts-for-same-repository.md | 45 +++++ ...evidence-import-bound-matcher-semantics.md | 3 +- ...evidence-import-bound-matcher-semantics.md | 3 +- ...sion-scoring-enriched-metadata-emission.md | 3 +- ...for-deterministic-CallExpression-policy.md | 3 +- ...or-prompt-metadata-aware-and-docs-first.md | 3 +- ...on-TypeScript-CallExpression-frameworks.md | 3 +- ...-contributor-authored-manual-confidence.md | 5 +- ...ore-adjustments-and-keep-match-evidence.md | 3 +- ...on-base_confidence-in-schema-and-loader.md | 3 +- ...ith-App-Interfaces-workflow-in-frontend.md | 3 +- ...l_args-tool_call_id-tool_result_content.md | 3 +- ...s-and-remove-Exa-only-launch-constraint.md | 4 +- ...ags-and-runtime-CMD-startup-performance.md | 52 ------ ...table-rows-caused-by-duplicate-row-keys.md | 4 +- ...back-sheet-spacing-and-selection-states.md | 3 +- ...nt-package-families-in-dependency-guide.md | 5 +- .../app_interfaces.md | 4 +- .../app_interfaces.md | 4 +- 27 files changed, 253 insertions(+), 89 deletions(-) create mode 100644 backlog/tasks/task-12 - Add-cancel-action-for-agent-workflows-in-Operations-Management.md create mode 100644 backlog/tasks/task-12.1 - Prevent-duplicate-AGENTS.md-run-starts-for-same-repository.md delete mode 100644 backlog/tasks/task-6 - Fix-flow-bridge-Dockerfile-uv-sync-flags-and-runtime-CMD-startup-performance.md diff --git a/backlog/tasks/task-1 - Implement-TypeScript-schema-extension-with-FunctionDefinition-export-name-filtering.md b/backlog/tasks/task-1 - Implement-TypeScript-schema-extension-with-FunctionDefinition-export-name-filtering.md index 3ce34997a..fcfc10fba 100644 --- a/backlog/tasks/task-1 - Implement-TypeScript-schema-extension-with-FunctionDefinition-export-name-filtering.md +++ b/backlog/tasks/task-1 - Implement-TypeScript-schema-extension-with-FunctionDefinition-export-name-filtering.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-02-25 06:16' -updated_date: '2026-03-04 11:24' +updated_date: '2026-03-10 09:31' labels: - typescript - schema @@ -28,7 +28,7 @@ documentation: - >- https://tree-sitter.github.io/py-tree-sitter/classes/tree_sitter.QueryCursor.html priority: high -ordinal: 6000 +ordinal: 7000 --- ## Description diff --git a/backlog/tasks/task-1.2 - Integrate-low-confidence-CallExpression-validator-agent-into-query-engine-workflow.md b/backlog/tasks/task-1.2 - Integrate-low-confidence-CallExpression-validator-agent-into-query-engine-workflow.md index 11f6d51f0..16e1dcfa5 100644 --- a/backlog/tasks/task-1.2 - Integrate-low-confidence-CallExpression-validator-agent-into-query-engine-workflow.md +++ b/backlog/tasks/task-1.2 - Integrate-low-confidence-CallExpression-validator-agent-into-query-engine-workflow.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-02-26 10:41' -updated_date: '2026-03-04 11:24' +updated_date: '2026-03-10 09:31' labels: - query-engine - validator @@ -46,7 +46,7 @@ documentation: - 'backlog://workflow/overview' parent_task_id: TASK-1 priority: high -ordinal: 4000 +ordinal: 5000 --- ## Description diff --git a/backlog/tasks/task-1.2.1 - Fix-low-confidence-validation-regressions-in-candidate-hydration-and-mapper-gating.md b/backlog/tasks/task-1.2.1 - Fix-low-confidence-validation-regressions-in-candidate-hydration-and-mapper-gating.md index 57fe39d44..c9e759e0f 100644 --- a/backlog/tasks/task-1.2.1 - Fix-low-confidence-validation-regressions-in-candidate-hydration-and-mapper-gating.md +++ b/backlog/tasks/task-1.2.1 - Fix-low-confidence-validation-regressions-in-candidate-hydration-and-mapper-gating.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-02-27 13:12' -updated_date: '2026-03-04 11:24' +updated_date: '2026-03-10 09:31' labels: - query-engine - validator @@ -32,7 +32,7 @@ documentation: CallExpression-Confidence-Scoring-and-Validation-Agent.md parent_task_id: TASK-1.2 priority: high -ordinal: 5000 +ordinal: 6000 --- ## Description diff --git a/backlog/tasks/task-10 - Re-architect-agent-events-for-scalable-history-and-virtualized-viewing.md b/backlog/tasks/task-10 - Re-architect-agent-events-for-scalable-history-and-virtualized-viewing.md index f621202d8..1d5cfe809 100644 --- a/backlog/tasks/task-10 - Re-architect-agent-events-for-scalable-history-and-virtualized-viewing.md +++ b/backlog/tasks/task-10 - Re-architect-agent-events-for-scalable-history-and-virtualized-viewing.md @@ -1,11 +1,11 @@ --- id: TASK-10 title: Re-architect agent events for scalable history and virtualized viewing -status: In Progress +status: Done assignee: - codex created_date: '2026-03-08 08:43' -updated_date: '2026-03-08 11:05' +updated_date: '2026-03-10 09:31' labels: - backend - frontend @@ -30,6 +30,7 @@ documentation: - 'https://tanstack.com/virtual/latest/docs/introduction' - 'https://electric-sql.com/docs/guides/shapes' priority: high +ordinal: 25000 --- ## Description diff --git a/backlog/tasks/task-10.1 - Backend-persist-agent-events-in-normalized-history-for-Electric-backed-frontend-sync.md b/backlog/tasks/task-10.1 - Backend-persist-agent-events-in-normalized-history-for-Electric-backed-frontend-sync.md index 263023baa..ebb53a6ad 100644 --- a/backlog/tasks/task-10.1 - Backend-persist-agent-events-in-normalized-history-for-Electric-backed-frontend-sync.md +++ b/backlog/tasks/task-10.1 - Backend-persist-agent-events-in-normalized-history-for-Electric-backed-frontend-sync.md @@ -3,11 +3,11 @@ id: TASK-10.1 title: >- Backend: persist agent events in normalized history for Electric-backed frontend sync -status: In Progress +status: Done assignee: - codex created_date: '2026-03-08 08:43' -updated_date: '2026-03-08 11:50' +updated_date: '2026-03-10 09:31' labels: - backend - performance @@ -25,6 +25,7 @@ references: /Users/jayghiya/.superset/worktrees/unoplat-code-confluence/update-commons-testing/unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/repo_models.py parent_task_id: TASK-10 priority: high +ordinal: 24000 --- ## Description diff --git a/backlog/tasks/task-10.2 - Frontend-replace-agent-events-accordion-body-with-a-virtualized-flat-timeline.md b/backlog/tasks/task-10.2 - Frontend-replace-agent-events-accordion-body-with-a-virtualized-flat-timeline.md index bd40ba511..743c73373 100644 --- a/backlog/tasks/task-10.2 - Frontend-replace-agent-events-accordion-body-with-a-virtualized-flat-timeline.md +++ b/backlog/tasks/task-10.2 - Frontend-replace-agent-events-accordion-body-with-a-virtualized-flat-timeline.md @@ -1,11 +1,11 @@ --- id: TASK-10.2 title: 'Frontend: replace agent events accordion body with a virtualized flat timeline' -status: In Progress +status: Done assignee: - codex created_date: '2026-03-08 08:43' -updated_date: '2026-03-08 12:22' +updated_date: '2026-03-10 09:31' labels: - frontend - performance @@ -26,6 +26,7 @@ references: /Users/jayghiya/.superset/worktrees/unoplat-code-confluence/update-commons-testing/unoplat-code-confluence-frontend/src/types/agent-events.ts parent_task_id: TASK-10 priority: high +ordinal: 22000 --- ## Description diff --git a/backlog/tasks/task-10.3 - Frontend-sync-agent-event-history-through-Electric-collection-and-merge-with-live-updates.md b/backlog/tasks/task-10.3 - Frontend-sync-agent-event-history-through-Electric-collection-and-merge-with-live-updates.md index b932f690e..ae4139c88 100644 --- a/backlog/tasks/task-10.3 - Frontend-sync-agent-event-history-through-Electric-collection-and-merge-with-live-updates.md +++ b/backlog/tasks/task-10.3 - Frontend-sync-agent-event-history-through-Electric-collection-and-merge-with-live-updates.md @@ -3,11 +3,11 @@ id: TASK-10.3 title: >- Frontend: sync agent event history through Electric collection and merge with live updates -status: In Progress +status: Done assignee: - codex created_date: '2026-03-08 08:43' -updated_date: '2026-03-08 13:11' +updated_date: '2026-03-10 09:31' labels: - frontend - performance @@ -28,6 +28,7 @@ references: /Users/jayghiya/.superset/worktrees/unoplat-code-confluence/update-commons-testing/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx parent_task_id: TASK-10 priority: high +ordinal: 21000 --- ## Description diff --git a/backlog/tasks/task-12 - Add-cancel-action-for-agent-workflows-in-Operations-Management.md b/backlog/tasks/task-12 - Add-cancel-action-for-agent-workflows-in-Operations-Management.md new file mode 100644 index 000000000..dba898958 --- /dev/null +++ b/backlog/tasks/task-12 - Add-cancel-action-for-agent-workflows-in-Operations-Management.md @@ -0,0 +1,154 @@ +--- +id: TASK-12 +title: Add cancel action for agent workflows in Operations Management +status: In Progress +assignee: [] +created_date: '2026-03-10 09:28' +updated_date: '2026-03-12 10:40' +labels: + - operations-management + - agent-md + - cancellation + - frontend + - query-engine + - ingestion +dependencies: [] +references: + - >- + unoplat-code-confluence-frontend/src/components/custom/SubmittedJobsDataTable.tsx + - >- + unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx + - unoplat-code-confluence-frontend/src/components/custom/JobStatusDialog.tsx + - >- + unoplat-code-confluence-frontend/src/components/custom/submitted-jobs-data-table-columns.tsx + - unoplat-code-confluence-frontend/src/lib/api.ts + - >- + unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/main.py + - >- + unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/models/github/github_repo.py + - >- + unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py + - >- + unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/temporal/workflow_service.py + - >- + unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/repo_models.py + - >- + unoplat-code-confluence-commons/src/unoplat_code_confluence_commons/workflow_models.py +documentation: + - 'https://python.temporal.io/temporalio.client.WorkflowHandle.html' + - 'https://docs.temporal.io/develop/python' +priority: high +ordinal: 3000 +--- + +## Description + + +Users need a way to stop a running AGENTS.md workflow from Operations Management. The cancel control must be available only for AGENTS_GENERATION and AGENT_MD_UPDATE runs, and must never be shown for INGESTION runs. This spans query-engine (cancel API + Temporal workflow cancellation), ingestion (job-list contract support for cancellability gating), and frontend (Operations Management action wiring and UX states). + + +## Acceptance Criteria + +- [ ] #1 Operations Management allows user-initiated cancellation for in-flight AGENTS_GENERATION and AGENT_MD_UPDATE runs. +- [ ] #2 Operations Management never displays a cancel action for INGESTION runs. +- [ ] #3 Query-engine provides a cancel endpoint that accepts repository owner/name/run identifier and returns deterministic responses for success, non-cancellable operation, not-found run, and already-terminal run. +- [ ] #4 Query-engine uses Temporal Python async cancellation (`await handle.cancel()`) for workflow cancellation. +- [ ] #5 Cancellation behavior does not require introducing a new workflow status enum value; existing status model remains valid and cancellation context is preserved in error/report metadata. +- [ ] #6 Frontend shows pending/disabled state during cancel request, then refreshes operations data and shows success or error feedback. +- [ ] #7 Ingestion job-list API provides enough data to gate cancel visibility safely in UI (existing operation/status fields or explicit cancellable flag). +- [ ] #8 Relevant API/docs/tests are updated to reflect the cancel workflow behavior and ingestion exclusion rule. + + +## Implementation Plan + + +## Scope +Enable Operations Management users to stop a running AGENTS.md workflow while guaranteeing that ingestion workflows never expose or accept cancellation. + +## Implementation Plan +1. Query-engine cancellation API +- Add a dedicated endpoint in `unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py` for repository agent run cancellation. +- Request contract: repository owner, repository name, repository workflow run id. +- Validate target run from `RepositoryWorkflowRun` (owner/name/run tuple). +- Enforce operation guardrail: reject `INGESTION` at API layer even if called directly. +- Enforce state guardrail: reject already-terminal runs (COMPLETED/FAILED/TIMED_OUT/ERROR) with deterministic response. +- Use Temporal async handle cancellation (`client.get_workflow_handle(...); await handle.cancel()`). +- Normalize Temporal RPC failures into stable API responses (not found, invalid state, generic failure). + +2. Cancellation status behavior +- Keep current `JobStatus` enum unchanged (no new `CANCELLED` status in this increment). +- Persist cancellation context via error metadata/report path (e.g., source=user_action, reason=cancel_requested) so UI and diagnostics can distinguish manual stop from runtime failures. +- Ensure eventual status transition after cancel request remains compatible with existing interceptors and terminal-state handling. + +3. Ingestion job-list contract alignment +- Update ingestion `/parent-workflow-jobs` response model and mapper to include explicit cancellability signal derived from operation + status (or formally document operation/status gating if explicit field is skipped). +- Rule: only AGENTS_GENERATION / AGENT_MD_UPDATE can be cancellable; INGESTION is always false. + +4. Frontend Operations Management wiring +- Add query-engine API client method for cancel action in `unoplat-code-confluence-frontend/src/lib/api.ts`. +- Extend job typing to carry cancellability signal in `unoplat-code-confluence-frontend/src/types.ts`. +- Add cancel CTA to AGENTS dialog flow in `unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx` only when run is cancellable and in-flight. +- Do not add cancel CTA in ingestion details dialog (`JobStatusDialog`). +- Add loading/disabled UX, success/failure toast, and query invalidation (`parentWorkflowJobs`, and any active run detail queries) after action. + +5. Verification and docs +- Add/adjust backend tests for cancel endpoint guards and success path. +- Add/adjust ingestion API tests for cancellability field derivation. +- Add/adjust frontend tests (or documented manual validation) for agent-only cancel visibility. +- Update interface docs where applicable (`app_interfaces.md` in touched services). + +## Delivery Notes +- Cancellation request acknowledgement does not guarantee immediate workflow termination; UI should reflect "cancel requested" semantics until a terminal status is observed. +- Backend enforcement is mandatory so ingestion cannot be cancelled even if a client bypasses UI controls. + + +## Implementation Notes + + +## Design Decisions Captured +- FastAPI endpoint is async; Temporal cancellation must use async call with `await handle.cancel()`. +- No new workflow status enum is introduced in this increment to avoid cross-service schema/constraint migration blast radius. +- Ingestion exclusion is enforced in both layers: UI visibility rules and backend API validation. + +## Files and Surfaces to Touch +- Query-engine endpoint and Temporal interaction: `unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py` +- Ingestion operations list contract: `unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/main.py`, `unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/models/github/github_repo.py` +- Frontend operation UX/API/types: `unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx`, `unoplat-code-confluence-frontend/src/components/custom/JobStatusDialog.tsx`, `unoplat-code-confluence-frontend/src/components/custom/SubmittedJobsDataTable.tsx`, `unoplat-code-confluence-frontend/src/lib/api.ts`, `unoplat-code-confluence-frontend/src/types.ts` + +## Operational Guardrails +- Reject cancel attempts when operation is `INGESTION`. +- Reject cancel attempts for terminal statuses. +- Return deterministic API messages for: run not found, already terminal, non-cancellable operation, cancel accepted, and backend failure. + +Implementation started in this session. + +Implemented query-engine cancel endpoint `POST /v1/repository-agent-run/cancel` with operation guardrails (agent-only), terminal-state guardrails, async Temporal cancel (`await handle.cancel()`), and deterministic HTTP responses for not found/non-cancellable/already-terminal/runtime unavailable. + +Implemented ingestion operations-list cancellability contract by adding `is_cancellable` to `ParentWorkflowJobResponse` and deriving it in `/parent-workflow-jobs` from operation + status. + +Implemented frontend wiring: added `cancelRepositoryAgentRun()` API client, added `is_cancellable` in `ParentWorkflowJobResponse` type, and added Cancel Run action in `GenerateAgentsDialog` for in-flight agent workflows only (not ingestion dialog). + +Updated interface documentation in query-engine and ingestion `app_interfaces.md`. + +Verification run: frontend `bun run build` passed. Targeted query-engine typecheck for modified endpoint passed. Targeted ingestion typecheck for modified model passed. Full-repo Python typechecks in query-engine/ingestion still report many pre-existing unrelated issues. + +Created child task `TASK-12.1` to address UX guardrail for preventing concurrent AGENTS.md run starts for the same repository. + +Simplified cancellation implementation per alpha scope: removed legacy/backward-compatibility recovery path and enforced strict workflow-id semantics. `repository_workflow_id` is now required at workflow-run initialization and DB activity raises if workflow_id is missing. + +Manual retest still resulted in `COMPLETED` after cancel. Continuing investigation into workflow cancellation propagation paths inside query-engine Temporal workflows. + +Used Temporal CLI on workflow `agent-fastapi-full-stack-fastapi-template-62517836` / run `019ce18e-5464-747b-a3a7-50aaa7fc8175` to confirm exact propagation bug: Temporal history ends in `WORKFLOW_EXECUTION_CANCELED`, but our interceptor scheduled parent DB status `COMPLETED` just before close. Root cause was repository interceptor not catching `asyncio.CancelledError` (inherits `BaseException`, not `Exception` in Python 3.13). + +Implemented precise fix: cancellation helpers now recognize both Temporal `CancelledError` and `asyncio.CancelledError`, and repository/codebase interceptor execute-workflow wrappers now explicitly catch `asyncio.CancelledError` so final DB status is written as `CANCELLED` instead of defaulting to `COMPLETED`. + +Refactored cancellation propagation logic to shared helper module `services/temporal/cancellation_helpers.py` to remove duplicated cause-chain parsing across workflow/interceptor. + +`temporal_workflows.py` now imports shared `is_temporal_cancellation_exception` and keeps only `_raise_if_temporal_cancellation` wrapper for workflow-local logging. + +`agent_workflow_interceptor.py` now imports the shared helper and uses `WORKFLOW_EXECUTION_EXCEPTIONS` constant to avoid duplicated except tuples. + +Added targeted tests `tests/services/test_cancellation_helpers.py` for direct Temporal cancel, direct asyncio cancel, wrapped cancel chain, and non-cancel exception. + +Verification: basedpyright (targeted files) passed; ruff check passed; pytest `tests/services/test_cancellation_helpers.py` passed (4 tests). + diff --git a/backlog/tasks/task-12.1 - Prevent-duplicate-AGENTS.md-run-starts-for-same-repository.md b/backlog/tasks/task-12.1 - Prevent-duplicate-AGENTS.md-run-starts-for-same-repository.md new file mode 100644 index 000000000..12c0f8e3a --- /dev/null +++ b/backlog/tasks/task-12.1 - Prevent-duplicate-AGENTS.md-run-starts-for-same-repository.md @@ -0,0 +1,45 @@ +--- +id: TASK-12.1 +title: Prevent duplicate AGENTS.md run starts for same repository +status: To Do +assignee: [] +created_date: '2026-03-10 11:03' +labels: + - frontend + - ux + - operations-management + - agent-md + - workflow-concurrency +dependencies: [] +references: + - >- + unoplat-code-confluence-frontend/src/components/custom/IngestedRepositoriesDataTable.tsx + - >- + unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx + - unoplat-code-confluence-frontend/src/lib/api.ts + - >- + unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py +parent_task_id: TASK-12 +priority: high +--- + +## Description + + +Operations UX should prevent users from starting multiple AGENTS.md generation/update runs for the same repository at the same time. Current behavior can trigger multiple concurrent repository agent workflows, which creates ambiguity for cancellation/status targeting and noisy operations history. + + +## Acceptance Criteria + +- [ ] #1 When a repository already has an in-flight AGENTS_GENERATION or AGENT_MD_UPDATE run, UI blocks starting another run for that same repository. +- [ ] #2 Start/Generate CTA shows clear disabled state and explanatory copy while a run is active. +- [ ] #3 Operations and repository status views stay consistent after refresh; no duplicate in-flight starts can be initiated via normal UI flow. +- [ ] #4 User can start a new AGENTS.md run again once previous run reaches terminal status. +- [ ] #5 Manual QA steps are documented for active-run, terminal-run, and refresh/navigation scenarios. + + +## Definition of Done + +- [ ] #1 Frontend behavior includes optimistic guard + server state recheck before start action +- [ ] #2 No regression for ingestion operation flows + diff --git a/backlog/tasks/task-3.2.1 - Implement-TypeScript-CallExpression-match-evidence-import-bound-matcher-semantics.md b/backlog/tasks/task-3.2.1 - Implement-TypeScript-CallExpression-match-evidence-import-bound-matcher-semantics.md index 4184f6171..7b35fb3a1 100644 --- a/backlog/tasks/task-3.2.1 - Implement-TypeScript-CallExpression-match-evidence-import-bound-matcher-semantics.md +++ b/backlog/tasks/task-3.2.1 - Implement-TypeScript-CallExpression-match-evidence-import-bound-matcher-semantics.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-05 09:56' -updated_date: '2026-03-05 10:17' +updated_date: '2026-03-10 09:31' labels: - typescript - framework-detection @@ -20,6 +20,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/engine/programming_language/typescript/test_typescript_additional_concept_extraction.py parent_task_id: TASK-3.2 priority: high +ordinal: 18000 --- ## Description diff --git a/backlog/tasks/task-3.2.2 - Implement-Python-CallExpression-match-evidence-import-bound-matcher-semantics.md b/backlog/tasks/task-3.2.2 - Implement-Python-CallExpression-match-evidence-import-bound-matcher-semantics.md index 8299e0aba..bd1739cb7 100644 --- a/backlog/tasks/task-3.2.2 - Implement-Python-CallExpression-match-evidence-import-bound-matcher-semantics.md +++ b/backlog/tasks/task-3.2.2 - Implement-Python-CallExpression-match-evidence-import-bound-matcher-semantics.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-05 09:56' -updated_date: '2026-03-05 10:09' +updated_date: '2026-03-10 09:31' labels: - python - framework-detection @@ -20,6 +20,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/parser/test_framework_detection_tree_sitter.py parent_task_id: TASK-3.2 priority: high +ordinal: 19000 --- ## Description diff --git a/backlog/tasks/task-3.2.3 - Implement-deterministic-CallExpression-scoring-enriched-metadata-emission.md b/backlog/tasks/task-3.2.3 - Implement-deterministic-CallExpression-scoring-enriched-metadata-emission.md index 47db2016e..77e7ff106 100644 --- a/backlog/tasks/task-3.2.3 - Implement-deterministic-CallExpression-scoring-enriched-metadata-emission.md +++ b/backlog/tasks/task-3.2.3 - Implement-deterministic-CallExpression-scoring-enriched-metadata-emission.md @@ -4,7 +4,7 @@ title: Implement deterministic CallExpression scoring + enriched metadata emissi status: Done assignee: [] created_date: '2026-03-05 09:56' -updated_date: '2026-03-05 10:27' +updated_date: '2026-03-10 09:31' labels: - confidence-scoring - framework-detection @@ -24,6 +24,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/processor/db/postgres/code_confluence_relational_ingestion.py parent_task_id: TASK-3.2 priority: high +ordinal: 16000 --- ## Description diff --git a/backlog/tasks/task-3.2.4 - Add-regression-tests-contributor-docs-for-deterministic-CallExpression-policy.md b/backlog/tasks/task-3.2.4 - Add-regression-tests-contributor-docs-for-deterministic-CallExpression-policy.md index 18c52c46f..d0e432232 100644 --- a/backlog/tasks/task-3.2.4 - Add-regression-tests-contributor-docs-for-deterministic-CallExpression-policy.md +++ b/backlog/tasks/task-3.2.4 - Add-regression-tests-contributor-docs-for-deterministic-CallExpression-policy.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-05 09:56' -updated_date: '2026-03-05 10:27' +updated_date: '2026-03-10 09:31' labels: - testing - documentation @@ -23,6 +23,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/framework-definitions/README.md parent_task_id: TASK-3.2 priority: high +ordinal: 17000 --- ## Description diff --git a/backlog/tasks/task-3.3.1 - Make-CallExpression-validator-prompt-metadata-aware-and-docs-first.md b/backlog/tasks/task-3.3.1 - Make-CallExpression-validator-prompt-metadata-aware-and-docs-first.md index d9ac10d8b..0e6a0e2d2 100644 --- a/backlog/tasks/task-3.3.1 - Make-CallExpression-validator-prompt-metadata-aware-and-docs-first.md +++ b/backlog/tasks/task-3.3.1 - Make-CallExpression-validator-prompt-metadata-aware-and-docs-first.md @@ -4,7 +4,7 @@ title: Make CallExpression validator prompt metadata-aware and docs-first status: Done assignee: [] created_date: '2026-03-06 11:18' -updated_date: '2026-03-06 11:34' +updated_date: '2026-03-10 09:31' labels: - query-engine - validator @@ -20,6 +20,7 @@ references: unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/models/repository/framework_feature_validation_models.py parent_task_id: TASK-3.3 priority: high +ordinal: 12000 --- ## Description diff --git a/backlog/tasks/task-3.4 - Calibrate-base_confidence-ambiguity-notes-for-current-Python-TypeScript-CallExpression-frameworks.md b/backlog/tasks/task-3.4 - Calibrate-base_confidence-ambiguity-notes-for-current-Python-TypeScript-CallExpression-frameworks.md index a41548360..88eb3d0eb 100644 --- a/backlog/tasks/task-3.4 - Calibrate-base_confidence-ambiguity-notes-for-current-Python-TypeScript-CallExpression-frameworks.md +++ b/backlog/tasks/task-3.4 - Calibrate-base_confidence-ambiguity-notes-for-current-Python-TypeScript-CallExpression-frameworks.md @@ -6,7 +6,7 @@ title: >- status: In Progress assignee: [] created_date: '2026-03-04 10:56' -updated_date: '2026-03-06 10:10' +updated_date: '2026-03-10 09:32' labels: - framework-definitions - confidence-scoring @@ -42,6 +42,7 @@ documentation: CallExpression-Confidence-Scoring-and-Validation-Agent.md parent_task_id: TASK-3 priority: high +ordinal: 2000 --- ## Description diff --git a/backlog/tasks/task-3.6 - Replace-detector-scored-CallExpression-confidence-with-contributor-authored-manual-confidence.md b/backlog/tasks/task-3.6 - Replace-detector-scored-CallExpression-confidence-with-contributor-authored-manual-confidence.md index e7227dfbd..caadf5754 100644 --- a/backlog/tasks/task-3.6 - Replace-detector-scored-CallExpression-confidence-with-contributor-authored-manual-confidence.md +++ b/backlog/tasks/task-3.6 - Replace-detector-scored-CallExpression-confidence-with-contributor-authored-manual-confidence.md @@ -3,10 +3,10 @@ id: TASK-3.6 title: >- Replace detector-scored CallExpression confidence with contributor-authored manual confidence -status: In Progress +status: Done assignee: [] created_date: '2026-03-06 05:58' -updated_date: '2026-03-06 10:10' +updated_date: '2026-03-10 09:32' labels: - framework-detection - call-expression @@ -26,6 +26,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/framework-definitions/README.md parent_task_id: TASK-3 priority: high +ordinal: 27000 --- ## Description diff --git a/backlog/tasks/task-3.6.1 - Rollback-detector-side-automated-CallExpression-score-adjustments-and-keep-match-evidence.md b/backlog/tasks/task-3.6.1 - Rollback-detector-side-automated-CallExpression-score-adjustments-and-keep-match-evidence.md index 64097fd9a..fc9d455be 100644 --- a/backlog/tasks/task-3.6.1 - Rollback-detector-side-automated-CallExpression-score-adjustments-and-keep-match-evidence.md +++ b/backlog/tasks/task-3.6.1 - Rollback-detector-side-automated-CallExpression-score-adjustments-and-keep-match-evidence.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-06 05:58' -updated_date: '2026-03-06 06:10' +updated_date: '2026-03-10 09:31' labels: - python - typescript @@ -24,6 +24,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/tests/engine/programming_language/typescript/test_typescript_additional_concept_extraction.py parent_task_id: TASK-3.6 priority: high +ordinal: 15000 --- ## Description diff --git a/backlog/tasks/task-3.6.2 - Enforce-explicit-manual-CallExpression-base_confidence-in-schema-and-loader.md b/backlog/tasks/task-3.6.2 - Enforce-explicit-manual-CallExpression-base_confidence-in-schema-and-loader.md index e63090c34..a8b89a881 100644 --- a/backlog/tasks/task-3.6.2 - Enforce-explicit-manual-CallExpression-base_confidence-in-schema-and-loader.md +++ b/backlog/tasks/task-3.6.2 - Enforce-explicit-manual-CallExpression-base_confidence-in-schema-and-loader.md @@ -4,7 +4,7 @@ title: Enforce explicit manual CallExpression base_confidence in schema and load status: Done assignee: [] created_date: '2026-03-06 06:01' -updated_date: '2026-03-06 10:10' +updated_date: '2026-03-10 09:31' labels: - schema - framework-definitions @@ -18,6 +18,7 @@ references: unoplat-code-confluence-ingestion/code-confluence-flow-bridge/src/code_confluence_flow_bridge/processor/db/postgres/framework_loader.py parent_task_id: TASK-3.6 priority: high +ordinal: 14000 --- ## Description diff --git a/backlog/tasks/task-3.7 - Align-validator-progress-naming-and-ordering-with-App-Interfaces-workflow-in-frontend.md b/backlog/tasks/task-3.7 - Align-validator-progress-naming-and-ordering-with-App-Interfaces-workflow-in-frontend.md index cab434551..b6714aaaf 100644 --- a/backlog/tasks/task-3.7 - Align-validator-progress-naming-and-ordering-with-App-Interfaces-workflow-in-frontend.md +++ b/backlog/tasks/task-3.7 - Align-validator-progress-naming-and-ordering-with-App-Interfaces-workflow-in-frontend.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-07 12:38' -updated_date: '2026-03-07 12:44' +updated_date: '2026-03-10 09:31' labels: - frontend - progress-ui @@ -28,6 +28,7 @@ documentation: - 'backlog://workflow/overview' parent_task_id: TASK-3 priority: high +ordinal: 11000 --- ## Description diff --git a/backlog/tasks/task-4.1 - Backend-Store-full-tool-context-in-events-JSONB-tool_name-tool_args-tool_call_id-tool_result_content.md b/backlog/tasks/task-4.1 - Backend-Store-full-tool-context-in-events-JSONB-tool_name-tool_args-tool_call_id-tool_result_content.md index 6af8bebaa..e41b20470 100644 --- a/backlog/tasks/task-4.1 - Backend-Store-full-tool-context-in-events-JSONB-tool_name-tool_args-tool_call_id-tool_result_content.md +++ b/backlog/tasks/task-4.1 - Backend-Store-full-tool-context-in-events-JSONB-tool_name-tool_args-tool_call_id-tool_result_content.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-02 08:01' -updated_date: '2026-03-02 12:14' +updated_date: '2026-03-10 09:31' labels: - backend - python @@ -21,6 +21,7 @@ references: unoplat-code-confluence-query-engine/src/unoplat_code_confluence_query_engine/services/tracking/repository_agent_snapshot_service.py parent_task_id: TASK-4 priority: high +ordinal: 20000 --- ## Description diff --git a/backlog/tasks/task-6 - Add-DuckDuckGo-fallback-for-Temporal-agents-and-remove-Exa-only-launch-constraint.md b/backlog/tasks/task-6 - Add-DuckDuckGo-fallback-for-Temporal-agents-and-remove-Exa-only-launch-constraint.md index f53478f89..8d45df48c 100644 --- a/backlog/tasks/task-6 - Add-DuckDuckGo-fallback-for-Temporal-agents-and-remove-Exa-only-launch-constraint.md +++ b/backlog/tasks/task-6 - Add-DuckDuckGo-fallback-for-Temporal-agents-and-remove-Exa-only-launch-constraint.md @@ -6,7 +6,7 @@ title: >- status: Done assignee: [] created_date: '2026-03-03 05:52' -updated_date: '2026-03-04 11:24' +updated_date: '2026-03-10 09:31' labels: - query-engine - agents @@ -15,7 +15,7 @@ labels: - frontend dependencies: [] priority: high -ordinal: 9000 +ordinal: 10000 --- ## Description diff --git a/backlog/tasks/task-6 - Fix-flow-bridge-Dockerfile-uv-sync-flags-and-runtime-CMD-startup-performance.md b/backlog/tasks/task-6 - Fix-flow-bridge-Dockerfile-uv-sync-flags-and-runtime-CMD-startup-performance.md deleted file mode 100644 index 3552bd034..000000000 --- a/backlog/tasks/task-6 - Fix-flow-bridge-Dockerfile-uv-sync-flags-and-runtime-CMD-startup-performance.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -id: TASK-6 -title: 'Fix flow-bridge Dockerfile: uv sync flags and runtime CMD startup performance' -status: To Do -assignee: [] -created_date: '2026-03-03 06:07' -labels: [] -dependencies: [] -references: - - unoplat-code-confluence-ingestion/code-confluence-flow-bridge/Dockerfile - - unoplat-code-confluence-ingestion/code-confluence-flow-bridge/pyproject.toml -documentation: - - 'https://docs.astral.sh/uv/guides/integration/fastapi/' - - 'https://docs.astral.sh/uv/guides/integration/docker/' -priority: high ---- - -## Description - - -The `code-confluence-flow-bridge` Docker container re-downloads packages from GitHub on every startup because `CMD ["uv", "run", ...]` triggers uv's environment sync check. Since `pyproject.toml` declares `unoplat-code-confluence-commons` as a git source dependency, uv performs a network fetch to GitHub on every container start. - -A secondary issue is that `uv sync --no-cache` disables uv's HTTP wheel cache (wastes bandwidth on rebuilds) and lacks `--locked` which is the official reproducibility flag. - -Official FastAPI + uv documentation explicitly recommends against `uv run` in production and prescribes `--locked --no-dev` with BuildKit cache mounts for the build stage. - -**Two targeted changes to `Dockerfile`:** - -1. Builder stage line 22 — replace `RUN uv sync --no-cache` with: - ```dockerfile - RUN --mount=type=cache,target=/root/.cache/uv \ - UV_LINK_MODE=copy uv sync --locked --no-dev - ``` - - `--locked`: fail if uv.lock is out of sync with pyproject.toml (reproducibility) - - `--no-dev`: exclude test/dev groups (mypy, basedpyright, pytest, testcontainers) from production image - - Cache mount + `UV_LINK_MODE=copy`: reuse uv HTTP/wheel cache across builds - -2. Runtime CMD line 63 — add `--frozen`: - ```dockerfile - CMD ["uv", "run", "--frozen", "uvicorn", "src.code_confluence_flow_bridge.main:app", "--host", "0.0.0.0", "--port", "8000"] - ``` - - `--frozen`: use the already-installed environment as-is; no sync or network access on startup - - -## Acceptance Criteria - -- [ ] #1 Container starts without downloading any packages or making network requests to GitHub -- [ ] #2 Running `docker build` a second time with no source changes reuses the uv cache and is significantly faster than the first build -- [ ] #3 Dev and test packages (mypy, basedpyright, pytest, testcontainers) are NOT present in the final image -- [ ] #4 `uv sync` in the builder stage fails (non-zero exit) if uv.lock is not in sync with pyproject.toml — verifiable by temporarily modifying pyproject.toml without updating the lockfile -- [ ] #5 The FastAPI app starts and serves requests correctly after the changes - diff --git a/backlog/tasks/task-7 - Fix-stale-GitHub-repository-table-rows-caused-by-duplicate-row-keys.md b/backlog/tasks/task-7 - Fix-stale-GitHub-repository-table-rows-caused-by-duplicate-row-keys.md index 2621371c0..0b7c20799 100644 --- a/backlog/tasks/task-7 - Fix-stale-GitHub-repository-table-rows-caused-by-duplicate-row-keys.md +++ b/backlog/tasks/task-7 - Fix-stale-GitHub-repository-table-rows-caused-by-duplicate-row-keys.md @@ -4,7 +4,7 @@ title: Fix stale GitHub repository table rows caused by duplicate row keys status: Done assignee: [] created_date: '2026-03-03 12:11' -updated_date: '2026-03-04 11:24' +updated_date: '2026-03-10 09:31' labels: - frontend - bugfix @@ -14,7 +14,7 @@ references: - >- unoplat-code-confluence-frontend/src/components/custom/RepositoryDataTable.tsx priority: high -ordinal: 7000 +ordinal: 23000 --- ## Description diff --git a/backlog/tasks/task-8 - Polish-app-feedback-sheet-spacing-and-selection-states.md b/backlog/tasks/task-8 - Polish-app-feedback-sheet-spacing-and-selection-states.md index c0d57fbbd..bdc71a56a 100644 --- a/backlog/tasks/task-8 - Polish-app-feedback-sheet-spacing-and-selection-states.md +++ b/backlog/tasks/task-8 - Polish-app-feedback-sheet-spacing-and-selection-states.md @@ -4,7 +4,7 @@ title: Polish app feedback sheet spacing and selection states status: Done assignee: [] created_date: '2026-03-06 10:11' -updated_date: '2026-03-06 10:59' +updated_date: '2026-03-10 09:31' labels: - frontend - design-polish @@ -17,6 +17,7 @@ references: - unoplat-code-confluence-frontend/src/forms/fields/emoji-rating-field.tsx - unoplat-code-confluence-frontend/src/components/ui/sheet.tsx priority: medium +ordinal: 13000 --- ## Description diff --git a/backlog/tasks/task-9 - Collapse-UI-component-package-families-in-dependency-guide.md b/backlog/tasks/task-9 - Collapse-UI-component-package-families-in-dependency-guide.md index 85e0cc7d3..03d99a44a 100644 --- a/backlog/tasks/task-9 - Collapse-UI-component-package-families-in-dependency-guide.md +++ b/backlog/tasks/task-9 - Collapse-UI-component-package-families-in-dependency-guide.md @@ -1,11 +1,11 @@ --- id: TASK-9 title: Collapse UI component package families in dependency guide -status: In Progress +status: Done assignee: - OpenCode created_date: '2026-03-08 05:24' -updated_date: '2026-03-08 07:33' +updated_date: '2026-03-10 09:31' labels: - dependency-guide - frontend @@ -34,6 +34,7 @@ documentation: - >- https://logfire-us.pydantic.dev/jayghiya/unoplat-code-confluence?q=trace_id%3D%27019cc6b086da24fda466c36da3475f50%27 priority: high +ordinal: 26000 --- ## Description diff --git a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/app_interfaces.md b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/app_interfaces.md index c60f4eedd..bf6bf4375 100644 --- a/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/app_interfaces.md +++ b/unoplat-code-confluence-ingestion/code-confluence-flow-bridge/app_interfaces.md @@ -7,7 +7,7 @@ - `GET /repository-status` — retrieve repository status (response: `GithubRepoStatus`). - `GET /repository-data` — fetch repository configuration (response: `GitHubRepoResponseConfiguration`). - `GET /codebase-metadata` — list codebase metadata (response: `CodebaseMetadataListResponse`). -- `GET /parent-workflow-jobs` — list parent workflow jobs (response: `ParentWorkflowJobListResponse`). +- `GET /parent-workflow-jobs` — list parent workflow jobs with cancellability metadata for operations UI gating (response: `ParentWorkflowJobListResponse`). - `GET /get/ingestedRepositories` — list ingested repositories (response: `IngestedRepositoriesListResponse`). - `POST /refresh-repository` — refresh repository ingestion (response: `RefreshRepositoryResponse`). - `DELETE /delete-repository` — delete repository from tracking. @@ -32,4 +32,4 @@ ### GitHub Issues Feedback (`src/code_confluence_flow_bridge/routers/github_issues/router.py`) - `POST /issues` — create issue feedback (response: `IssueTracking`). -- `POST /feedback` — create feedback issue (response: `IssueTracking`). \ No newline at end of file +- `POST /feedback` — create feedback issue (response: `IssueTracking`). diff --git a/unoplat-code-confluence-query-engine/app_interfaces.md b/unoplat-code-confluence-query-engine/app_interfaces.md index 92f31351f..3bce7ffef 100644 --- a/unoplat-code-confluence-query-engine/app_interfaces.md +++ b/unoplat-code-confluence-query-engine/app_interfaces.md @@ -7,5 +7,5 @@ | Model config + providers | `GET /model-config`, `PUT /model-config`, `DELETE /model-config`, `GET /providers`, `GET /providers/{provider_key}` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/ai_model_config.py` | Manage provider model configuration and provider catalog exposure. | | Codex OpenAI OAuth | `POST /model-config/codex-openai/oauth/authorize`, `GET /model-config/codex-openai/oauth/flows/{flow_id}`, `GET /model-config/codex-openai/oauth/callback`, `GET /model-config/codex-openai/oauth/status`, `DELETE /model-config/codex-openai/oauth`, `GET /auth/callback` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/ai_model_config.py` | OAuth flow lifecycle + callback handlers for Codex OpenAI provider configuration. | | Feature flags | `GET /flags`, `GET /flags/{flag_name}`, `PUT /flags/{flag_name}`, `DELETE /flags/{flag_name}` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/flags.py` | Runtime flag configuration and lookup. | -| Repository agent rules | `GET /codebase-agent-rules`, `GET /repository-agent-snapshot`, `POST /repository-agent-md-pr`, `GET /repository-agent-md-pr` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py` | Rules metadata, snapshot payloads, and markdown PR workflows for repository agents. | -| Tool configuration | `GET /{provider}`, `PUT /{provider}`, `DELETE /{provider}`, `GET /` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/tool_config.py` | Provider-specific tool config CRUD and listing. | \ No newline at end of file +| Repository agent rules | `GET /codebase-agent-rules`, `POST /repository-agent-run/cancel`, `GET /repository-agent-snapshot`, `POST /repository-agent-md-pr`, `GET /repository-agent-md-pr` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/codebase_agent_rules.py` | Rules metadata, run cancellation, snapshot payloads, and markdown PR workflows for repository agents. | +| Tool configuration | `GET /{provider}`, `PUT /{provider}`, `DELETE /{provider}`, `GET /` | `src/unoplat_code_confluence_query_engine/api/v1/endpoints/tool_config.py` | Provider-specific tool config CRUD and listing. | From e9640f562c7c4178c502cfa0ed830080487078c2 Mon Sep 17 00:00:00 2001 From: JayGhiya Date: Fri, 13 Mar 2026 12:37:08 +0530 Subject: [PATCH 4/4] fix(agent-feedback): replace emoji sheet with thumbs dialog and redesign preview Migrate agent feedback from Sheet+emoji rating to Dialog+thumbs up/down. Add scrollable layout, sticky header/footer, soft/card toggle variants to UI primitives. Redesign GenerateAgentsPreview with tech stack badges, copy-all action, and proper dialog footer. Co-Authored-By: Claude Opus 4.6 --- .../custom/GenerateAgentsDialog.tsx | 14 +- .../custom/GenerateAgentsPreview.tsx | 227 +++++++++++++----- .../src/components/ui/dialog.tsx | 61 ++++- .../src/components/ui/toggle.tsx | 4 + ...ck-sheet.tsx => agent-feedback-dialog.tsx} | 49 ++-- .../components/details-step.tsx | 10 +- .../components/mini-emoji-selector.tsx | 57 ----- .../agent-feedback/components/rating-step.tsx | 204 ++++++++-------- .../components/thumbs-rating-selector.tsx | 60 +++++ .../src/features/agent-feedback/index.ts | 3 +- .../src/features/agent-feedback/schema.ts | 24 +- .../src/features/agent-feedback/store.ts | 2 +- .../src/forms/fields/emoji-rating-field.tsx | 17 +- .../src/forms/fields/index.ts | 2 +- .../src/forms/fields/mini-emoji-field.tsx | 66 ----- .../src/forms/fields/thumbs-rating-field.tsx | 39 +++ .../src/forms/form-hook.ts | 4 +- .../src/forms/index.ts | 2 +- 18 files changed, 487 insertions(+), 358 deletions(-) rename unoplat-code-confluence-frontend/src/features/agent-feedback/components/{agent-feedback-sheet.tsx => agent-feedback-dialog.tsx} (82%) delete mode 100644 unoplat-code-confluence-frontend/src/features/agent-feedback/components/mini-emoji-selector.tsx create mode 100644 unoplat-code-confluence-frontend/src/features/agent-feedback/components/thumbs-rating-selector.tsx delete mode 100644 unoplat-code-confluence-frontend/src/forms/fields/mini-emoji-field.tsx create mode 100644 unoplat-code-confluence-frontend/src/forms/fields/thumbs-rating-field.tsx diff --git a/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx b/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx index d9b308f67..aac404eea 100644 --- a/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx +++ b/unoplat-code-confluence-frontend/src/components/custom/GenerateAgentsDialog.tsx @@ -44,7 +44,7 @@ import { } from "@/lib/api"; import { apiToUiErrorReport } from "@/lib/error-utils"; import { FeedbackDialog } from "@/components/custom/FeedbackDialog"; -import { AgentFeedbackSheet } from "@/features/agent-feedback"; +import { AgentFeedbackDialog } from "@/features/agent-feedback"; import { ButtonGroup } from "@/components/ui/button-group"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { XCircle, ExternalLink, MessageSquare } from "lucide-react"; @@ -116,7 +116,7 @@ export function GenerateAgentsDialog({ React.useState(false); const [feedbackSource, setFeedbackSource] = React.useState(null); - const [agentFeedbackSheetOpen, setAgentFeedbackSheetOpen] = + const [agentFeedbackDialogOpen, setAgentFeedbackDialogOpen] = React.useState(false); const queryClient = useQueryClient(); @@ -769,7 +769,7 @@ export function GenerateAgentsDialog({ + +
+ Codebases
- - + + {codebaseEntries.map(([codebaseName, agentMdOutput]) => { const codebaseMarkdown = agentMdOutputToMarkdown(agentMdOutput, { title: codebaseName, }); + const editorKey = buildMarkdownEditorKey( + codebaseName, + codebaseMarkdown, + ); + const techStack = buildTechStackSubtitle( + agentMdOutput.programming_language_metadata, + ); return ( -
- - - {codebaseName} - - +
+
+ +
+
+ +
+ + {techStack && ( + + )} +
+
+
- +
-
@@ -144,6 +230,27 @@ export function GenerateAgentsPreview({ })} + + + +
+ + +
+
); diff --git a/unoplat-code-confluence-frontend/src/components/ui/dialog.tsx b/unoplat-code-confluence-frontend/src/components/ui/dialog.tsx index bde03d8e8..ab964ab53 100644 --- a/unoplat-code-confluence-frontend/src/components/ui/dialog.tsx +++ b/unoplat-code-confluence-frontend/src/components/ui/dialog.tsx @@ -46,7 +46,7 @@ function DialogOverlay({ } const dialogContentVariants = cva( - "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200", + "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200", { variants: { size: { @@ -67,11 +67,16 @@ const dialogContentVariants = cva( none: "gap-0", default: "gap-4", }, + layout: { + default: "grid", + scrollable: "flex flex-col max-h-[80vh] overflow-hidden", + }, }, defaultVariants: { size: "default", padding: "default", gap: "default", + layout: "default", }, }, ); @@ -83,6 +88,7 @@ function DialogContent({ size, padding, gap, + layout, ...props }: React.ComponentProps & VariantProps & { @@ -93,7 +99,10 @@ function DialogContent({ {children} @@ -111,24 +120,56 @@ function DialogContent({ ); } -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { +const dialogHeaderVariants = cva( + "flex flex-col gap-2 text-center sm:text-left", + { + variants: { + variant: { + default: "", + sticky: "shrink-0 border-b px-6 py-4", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function DialogHeader({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { return (
); } -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { +const dialogFooterVariants = cva("flex", { + variants: { + variant: { + default: "flex-col-reverse gap-2 sm:flex-row sm:justify-end", + sticky: "flex-row justify-end gap-2 border-t px-6 py-4", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +function DialogFooter({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { return (
); @@ -172,4 +213,6 @@ export { DialogTitle, DialogTrigger, dialogContentVariants, + dialogFooterVariants, + dialogHeaderVariants, }; diff --git a/unoplat-code-confluence-frontend/src/components/ui/toggle.tsx b/unoplat-code-confluence-frontend/src/components/ui/toggle.tsx index cad2dea8a..23cc3e815 100644 --- a/unoplat-code-confluence-frontend/src/components/ui/toggle.tsx +++ b/unoplat-code-confluence-frontend/src/components/ui/toggle.tsx @@ -12,10 +12,14 @@ const toggleVariants = cva( default: "bg-transparent", outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", + soft: "bg-transparent focus-visible:ring-offset-0 data-[state=on]:bg-primary/10 data-[state=on]:text-foreground data-[state=on]:ring-inset data-[state=on]:ring-1 data-[state=on]:ring-primary", + card: "border border-border text-muted-foreground ring-inset ring-offset-0 focus-visible:ring-offset-0 transition-[color,background-color,border-color,box-shadow] hover:border-primary/30 hover:bg-primary/5 hover:text-foreground data-[state=on]:border-primary/70 data-[state=on]:bg-primary/10 data-[state=on]:text-foreground data-[state=on]:ring-2 data-[state=on]:ring-primary/70 data-[state=on]:hover:border-primary data-[state=on]:hover:bg-primary/15 data-[state=on]:hover:text-foreground", }, size: { default: "h-10 px-3 min-w-10", sm: "h-9 px-2.5 min-w-9", + icon: "h-8 w-8 min-w-8 p-0 rounded-lg", + card: "flex h-auto flex-1 flex-col gap-2 px-4 py-3.5 rounded-xl", lg: "h-11 px-5 min-w-11", }, }, diff --git a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/agent-feedback-sheet.tsx b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/agent-feedback-dialog.tsx similarity index 82% rename from unoplat-code-confluence-frontend/src/features/agent-feedback/components/agent-feedback-sheet.tsx rename to unoplat-code-confluence-frontend/src/features/agent-feedback/components/agent-feedback-dialog.tsx index 75d2cb37e..d47b2de14 100644 --- a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/agent-feedback-sheet.tsx +++ b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/agent-feedback-dialog.tsx @@ -1,12 +1,12 @@ import { useCallback, useState } from "react"; import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import type { IssueTracking, ParentWorkflowJobResponse } from "@/types"; import type { RepositoryAgentCodebaseState } from "@/features/repository-agent-snapshots/transformers"; @@ -16,8 +16,8 @@ import { DetailsStep } from "./details-step"; import { RatingStep } from "./rating-step"; import { SuccessStep } from "./success-step"; -interface AgentFeedbackSheetProps { - /** Whether the sheet is open */ +interface AgentFeedbackDialogProps { + /** Whether the dialog is open */ open: boolean; /** Callback when open state changes */ onOpenChange: (open: boolean) => void; @@ -28,7 +28,7 @@ interface AgentFeedbackSheetProps { } /** - * Agent feedback sheet component + * Agent feedback dialog component * * Pure coordinator that orchestrates the multi-step feedback flow. * Each step (RatingStep, DetailsStep) owns its own form via useAppForm. @@ -36,12 +36,12 @@ interface AgentFeedbackSheetProps { * * Flow: Rating Step → Details Step → Success Step */ -export function AgentFeedbackSheet({ +export function AgentFeedbackDialog({ open, onOpenChange, job, codebases, -}: AgentFeedbackSheetProps): React.ReactElement { +}: AgentFeedbackDialogProps): React.ReactElement { // Zustand store for step navigation and draft persistence const { step, setStep, reset: resetStore } = useAgentFeedbackStore(); @@ -70,7 +70,7 @@ export function AgentFeedbackSheet({ [setStep], ); - // Handle sheet close + // Handle dialog close const handleClose = useCallback((): void => { // On success step, reset the store since feedback was submitted if (step === FeedbackStep.SUCCESS) { @@ -137,8 +137,7 @@ export function AgentFeedbackSheet({ case FeedbackStep.RATING: return { title: "Rate Your Experience", - description: - "Help us improve agent generation by sharing your feedback", + description: "Help us improve context by sharing your feedback", }; case FeedbackStep.DETAILS: return { @@ -153,7 +152,7 @@ export function AgentFeedbackSheet({ default: return { title: "Agent Feedback", - description: "Share your experience with agent generation", + description: "Share your experience with code confluence agents", }; } }; @@ -161,19 +160,15 @@ export function AgentFeedbackSheet({ const { title, description } = getStepHeader(); return ( - - - - {title} - {description} - + + + + {title} + {description} + {renderStepContent()} - - + + ); } diff --git a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/details-step.tsx b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/details-step.tsx index ca927b2a8..f5b1af8b3 100644 --- a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/details-step.tsx +++ b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/details-step.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { SheetFooter } from "@/components/ui/sheet"; +import { DialogFooter } from "@/components/ui/dialog"; import { useAppForm } from "@/forms"; import type { IssueTracking, ParentWorkflowJobResponse } from "@/types"; @@ -118,8 +118,8 @@ export function DetailsStep({ return ( <> - -
+ +
{/* Selected Rating Summary Card */} @@ -188,7 +188,7 @@ export function DetailsStep({
- + @@ -207,7 +207,7 @@ export function DetailsStep({ )} - + ); } diff --git a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/mini-emoji-selector.tsx b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/mini-emoji-selector.tsx deleted file mode 100644 index 9a6f3692e..000000000 --- a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/mini-emoji-selector.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; -import { cn } from "@/lib/utils"; - -import type { SentimentRating } from "../schema"; -import { getSentimentEmoji, getSentimentLabel } from "../utils"; - -interface MiniEmojiSelectorProps { - /** Currently selected rating value (can be null for unrated) */ - value: SentimentRating | null; - /** Callback when rating selection changes */ - onChange: (rating: SentimentRating | null) => void; - /** Optional className for container */ - className?: string; -} - -const RATINGS: SentimentRating[] = ["happy", "neutral", "unhappy"]; - -/** - * Compact emoji-based sentiment rating selector for per-agent ratings - * - * Similar to EmojiRatingSelector but with smaller dimensions for use - * in collapsible per-agent rating sections. Allows deselection (null value). - */ -export function MiniEmojiSelector({ - value, - onChange, - className, -}: MiniEmojiSelectorProps): React.ReactElement { - return ( - { - // Allow deselection by passing null when value is empty - onChange(newValue ? (newValue as SentimentRating) : null); - }} - className={cn("flex items-center gap-1", className)} - > - {RATINGS.map((rating) => ( - - - - ))} - - ); -} diff --git a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/rating-step.tsx b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/rating-step.tsx index 95f2be484..9f009fcca 100644 --- a/unoplat-code-confluence-frontend/src/features/agent-feedback/components/rating-step.tsx +++ b/unoplat-code-confluence-frontend/src/features/agent-feedback/components/rating-step.tsx @@ -1,28 +1,31 @@ -import { useMemo, useState } from "react"; -import { ChevronDown } from "lucide-react"; +import { useMemo } from "react"; import { SiGithub } from "react-icons/si"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader } from "@/components/ui/card"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; +import { Card, CardContent } from "@/components/ui/card"; +import { DialogFooter } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { SheetFooter } from "@/components/ui/sheet"; import { useAppForm, useFieldContext } from "@/forms"; -import { cn } from "@/lib/utils"; import type { ParentWorkflowJobResponse } from "@/types"; import type { RepositoryAgentCodebaseState } from "@/features/repository-agent-snapshots/transformers"; -import type { AgentId, AgentRatingValue, SentimentRating } from "../schema"; +import type { + AgentId, + AgentRatingValue, + AgentSentimentRating, + SentimentRating, +} from "../schema"; import { AGENT_IDS, AGENT_ID_LABELS } from "../schema"; import { useAgentFeedbackStore } from "../store"; -import { MiniEmojiSelector } from "./mini-emoji-selector"; +import { ThumbsRatingSelector } from "./thumbs-rating-selector"; interface RatingStepProps { /** Codebases from Electric SQL snapshot */ @@ -39,7 +42,7 @@ interface RatingStepProps { * Rating step - First step of feedback flow * * Owns its form via `useAppForm` and syncs values to Zustand store on navigation. - * Uses context-based field access for nested components (CodebaseRatingCard, AgentRatingRow). + * Uses context-based field access for nested components (CodebaseAccordionItem, AgentRatingRow). */ export function RatingStep({ codebases, @@ -47,7 +50,6 @@ export function RatingStep({ onCancel, onContinue, }: RatingStepProps): React.ReactElement { - const [agentRatingsOpen, setAgentRatingsOpen] = useState(true); const { draft, setDraft } = useAgentFeedbackStore(); // Build initial agent ratings array from codebases + stored draft @@ -89,81 +91,59 @@ export function RatingStep({ return ( <> - -
- {/* Repository Info Card */} - - -
- -
-
- - {job.repository_owner_name}/{job.repository_name} - - - {codebases.length} codebase{codebases.length !== 1 ? "s" : ""}{" "} - analyzed - -
-
-
- - - - {/* Overall Rating Section */} -
- - - {(field) => } - -
- - + + {/* Repository Info Card */} + + +
+ +
+
+ + {job.repository_owner_name}/{job.repository_name} + + + {codebases.length} codebase{codebases.length !== 1 ? "s" : ""}{" "} + analyzed + +
+
+
+ + {/* Overall Rating Section */} +
+ + + {(field) => } + +
- {/* Per-Agent Ratings Collapsible */} - - - - - - {/* Wrap with agentRatings field context for nested components */} - - {() => ( - <> - {codebases.map((codebase) => ( - - ))} - - )} - - - + {codebases.map((codebase) => ( + + ))} + + )} +
- + @@ -174,37 +154,46 @@ export function RatingStep({ )} - + ); } // ───────────────────────────────────────────────── -// Sub-component: CodebaseRatingCard +// Sub-component: CodebaseAccordionItem // ───────────────────────────────────────────────── -interface CodebaseRatingCardProps { +interface CodebaseAccordionItemProps { codebase: RepositoryAgentCodebaseState; } /** - * Card showing agent ratings for a single codebase + * Accordion item showing agent ratings for a single codebase. * - * No form prop needed - nested AgentRatingRow components access - * field state via useFieldContext (context set by parent form.AppField). + * Reads the agentRatings array via useFieldContext to compute + * the progress badge count (rated / total agents). */ -function CodebaseRatingCard({ +function CodebaseAccordionItem({ codebase, -}: CodebaseRatingCardProps): React.ReactElement { +}: CodebaseAccordionItemProps): React.ReactElement { + const field = useFieldContext(); + + // Count non-null ratings for this codebase + const ratedCount = field.state.value.filter( + (ar) => ar.codebase_name === codebase.codebaseName && ar.rating !== null, + ).length; + return ( - - + +
- CODEBASE - {codebase.codebaseName} + {codebase.codebaseName} + + {ratedCount}/{AGENT_IDS.length} +
-
- + + {AGENT_IDS.map((agentId) => ( ))} - -
+ + ); } @@ -227,7 +216,7 @@ interface AgentRatingRowProps { } /** - * Single row for rating one agent + * Single row for rating one agent with thumbs up/down. * * Uses `useFieldContext` to access the agentRatings array field * without requiring form prop drilling. Must be rendered inside @@ -244,9 +233,10 @@ function AgentRatingRow({ const index = field.state.value.findIndex( (ar) => ar.codebase_name === codebaseName && ar.agent_id === agentId, ); - const currentRating = index >= 0 ? field.state.value[index]?.rating : null; + const currentRating = + index >= 0 ? (field.state.value[index]?.rating ?? null) : null; - const handleChange = (rating: SentimentRating | null): void => { + const handleChange = (rating: AgentSentimentRating | null): void => { if (index >= 0) { const updated = [...field.state.value]; updated[index] = { ...updated[index], rating }; @@ -255,11 +245,11 @@ function AgentRatingRow({ }; return ( -
-