diff --git a/backend/routes/ws_playground.py b/backend/routes/ws_playground.py
index cc642d4..337a28c 100644
--- a/backend/routes/ws_playground.py
+++ b/backend/routes/ws_playground.py
@@ -152,6 +152,9 @@ async def websocket_playground_index(websocket: WebSocket, job_id: str):
job_id=job_id[:12],
event_type=event_type
)
+ # Small delay to ensure client processes message before close
+ # This prevents race condition where onclose fires before onmessage
+ await asyncio.sleep(0.2)
break
except json.JSONDecodeError:
diff --git a/backend/services/anonymous_indexer.py b/backend/services/anonymous_indexer.py
index 9ffacb8..e5a47f1 100644
--- a/backend/services/anonymous_indexer.py
+++ b/backend/services/anonymous_indexer.py
@@ -43,9 +43,9 @@ def to_dict(self) -> dict:
@dataclass
class JobStats:
"""Final stats for completed job."""
- files_indexed: int = 0
- functions_found: int = 0
- time_taken_seconds: float = 0
+ files_processed: int = 0
+ functions_indexed: int = 0
+ indexing_time_seconds: float = 0
def to_dict(self) -> dict:
return asdict(self)
@@ -345,12 +345,18 @@ async def run_indexing_job(
job_manager.update_status(job_id, JobStatus.PROCESSING)
# Progress callback for real-time updates
- async def progress_callback(files_processed: int, functions_found: int, total: int):
+ async def progress_callback(
+ files_processed: int,
+ functions_found: int,
+ total: int,
+ current_file: Optional[str] = None
+ ):
job_manager.update_progress(
job_id,
files_processed=files_processed,
functions_found=functions_found,
- files_total=total
+ files_total=total,
+ current_file=current_file
)
# Run indexing with timeout
@@ -370,9 +376,9 @@ async def progress_callback(files_processed: int, functions_found: int, total: i
# --- Step 3: Mark complete ---
elapsed = time.time() - start_time
stats = JobStats(
- files_indexed=file_count,
- functions_found=total_functions,
- time_taken_seconds=round(elapsed, 2)
+ files_processed=file_count,
+ functions_indexed=total_functions,
+ indexing_time_seconds=round(elapsed, 2)
)
job_manager.update_status(
diff --git a/backend/services/indexer_optimized.py b/backend/services/indexer_optimized.py
index 4eb9db7..521935e 100644
--- a/backend/services/indexer_optimized.py
+++ b/backend/services/indexer_optimized.py
@@ -672,8 +672,11 @@ async def index_repository_with_progress(
files_processed = min(i + self.FILE_BATCH_SIZE, total_files)
- # Send progress update
- await progress_callback(files_processed, len(all_functions_data), total_files)
+ # Get the last file in this batch for display
+ current_file = batch[-1].name if batch else None
+
+ # Send progress update with current file
+ await progress_callback(files_processed, len(all_functions_data), total_files, current_file)
logger.debug("Processing files",
processed=files_processed,
diff --git a/backend/tests/test_anonymous_indexing.py b/backend/tests/test_anonymous_indexing.py
index 3fe7db6..2bf97de 100644
--- a/backend/tests/test_anonymous_indexing.py
+++ b/backend/tests/test_anonymous_indexing.py
@@ -199,9 +199,9 @@ def test_update_status_completed_with_stats(self, job_manager, mock_redis):
})
stats = JobStats(
- files_indexed=100,
- functions_found=500,
- time_taken_seconds=45.5
+ files_processed=100,
+ functions_indexed=500,
+ indexing_time_seconds=45.5
)
result = job_manager.update_status(
@@ -259,13 +259,13 @@ def test_job_progress_none_excluded(self):
def test_job_stats_to_dict(self):
"""JobStats converts to dict correctly."""
stats = JobStats(
- files_indexed=100,
- functions_found=500,
- time_taken_seconds=45.5
+ files_processed=100,
+ functions_indexed=500,
+ indexing_time_seconds=45.5
)
d = stats.to_dict()
- assert d["files_indexed"] == 100
- assert d["time_taken_seconds"] == 45.5
+ assert d["files_processed"] == 100
+ assert d["indexing_time_seconds"] == 45.5
# =============================================================================
@@ -608,9 +608,9 @@ def test_completed_job_returns_repo_id(self, mock_job_class, client):
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:01:00Z",
"stats": {
- "files_indexed": 100,
- "functions_found": 500,
- "time_taken_seconds": 45.2
+ "files_processed": 100,
+ "functions_indexed": 500,
+ "indexing_time_seconds": 45.2
}
}
mock_job_class.return_value = mock_job_manager
@@ -621,7 +621,7 @@ def test_completed_job_returns_repo_id(self, mock_job_class, client):
data = response.json()
assert data["status"] == "completed"
assert data["repo_id"] == "anon_idx_test123456"
- assert data["stats"]["files_indexed"] == 100
+ assert data["stats"]["files_processed"] == 100
@patch('routes.playground.AnonymousIndexingJob')
def test_failed_job_returns_error(self, mock_job_class, client):
diff --git a/backend/tests/test_ws_playground.py b/backend/tests/test_ws_playground.py
index c558981..3148fb7 100644
--- a/backend/tests/test_ws_playground.py
+++ b/backend/tests/test_ws_playground.py
@@ -66,7 +66,7 @@ def test_websocket_handles_already_completed_job(self):
"job_id": "idx_test123",
"status": "completed",
"repo_id": "anon_test123",
- "stats": {"files_indexed": 100, "functions_found": 500}
+ "stats": {"files_processed": 100, "functions_indexed": 500}
})
with patch('routes.ws_playground.redis_client', mock_redis):
@@ -172,9 +172,9 @@ def test_completed_event_includes_stats(self):
job_manager = AnonymousIndexingJob(mock_redis)
stats = JobStats(
- files_indexed=100,
- functions_found=500,
- time_taken_seconds=45.2
+ files_processed=100,
+ functions_indexed=500,
+ indexing_time_seconds=45.2
)
job_manager.update_status(
@@ -189,7 +189,7 @@ def test_completed_event_includes_stats(self):
assert event_data["type"] == "completed"
assert event_data["repo_id"] == "anon_test123"
- assert event_data["stats"]["functions_found"] == 500
+ assert event_data["stats"]["functions_indexed"] == 500
def test_processing_status_skips_duplicate_publish(self):
"""PROCESSING status should not publish (handled by update_progress)."""
diff --git a/frontend/src/components/playground/HeroPlayground.tsx b/frontend/src/components/playground/HeroPlayground.tsx
index aba209c..67f99c9 100644
--- a/frontend/src/components/playground/HeroPlayground.tsx
+++ b/frontend/src/components/playground/HeroPlayground.tsx
@@ -169,11 +169,13 @@ export function HeroPlayground({
}, [state]);
// Determine visibility states
+ const hasError = state.status === 'error' || wsHasError;
const showDemoSelector = mode === 'demo';
- const showUrlInput = mode === 'custom' && !['indexing', 'ready'].includes(state.status) && !showCelebration && !wsIsCompleted;
- const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status) && !showCelebration;
- const showIndexing = mode === 'custom' && state.status === 'indexing' && !showCelebration && !wsIsCompleted;
+ const showUrlInput = mode === 'custom' && !['indexing', 'ready', 'error'].includes(state.status) && !showCelebration && !wsIsCompleted && !wsHasError;
+ const showValidation = mode === 'custom' && ['validating', 'valid', 'invalid'].includes(state.status) && !showCelebration && !hasError;
+ const showIndexing = mode === 'custom' && state.status === 'indexing' && !showCelebration && !wsIsCompleted && !wsHasError;
const showReady = (mode === 'custom' && state.status === 'ready') || (wsIsCompleted && !showCelebration);
+ const showError = mode === 'custom' && hasError && !showCelebration;
const isSearchDisabled = mode === 'custom' && state.status !== 'ready' && !wsIsCompleted;
// Can search?
@@ -350,6 +352,34 @@ export function HeroPlayground({
)}
+
+ {/* Error State - Inside AnimatePresence to prevent simultaneous renders */}
+ {showError && (
+ Something went wrong
+ {state.status === 'error' ? state.message : wsError || 'An unexpected error occurred'}
+
- {state.status === 'error' ? state.message : wsError}
-