Skip to content

Commit 6ac9c7a

Browse files
CopilotMte90
andauthored
Add FileWatcher for project monitoring, expose indexing stats to UI/API, and fix project deletion (#3)
Co-authored-by: Mte90 <403283+Mte90@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent a07b16c commit 6ac9c7a

File tree

8 files changed

+558
-15
lines changed

8 files changed

+558
-15
lines changed

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ CODING_MODEL=gpt-4o-code-preview
3030
# Host (default 127.0.0.1) and port (default 8000) used when launching uvicorn
3131
UVICORN_HOST=127.0.0.1
3232
UVICORN_PORT=8080
33+
34+
# FileWatcher configuration
35+
# Enable/disable the background file watcher that monitors project changes (default: true)
36+
FILE_WATCHER_ENABLED=true
37+
38+
# Interval in seconds between directory scans (default: 10, minimum: 5)
39+
FILE_WATCHER_INTERVAL=10
40+
41+
# Debounce time in seconds before processing detected changes (default: 5, minimum: 1)
42+
FILE_WATCHER_DEBOUNCE=5

REST_API.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Returns list of all registered projects.
101101
GET /api/projects/{project_id}
102102
```
103103

104-
Returns details for a specific project.
104+
Returns details for a specific project including indexing statistics.
105105

106106
**Response:**
107107
```json
@@ -112,10 +112,20 @@ Returns details for a specific project.
112112
"database_path": "~/.picocode/projects/1234567890abcdef.db",
113113
"status": "ready",
114114
"created_at": "2025-11-06T14:30:00",
115-
"last_indexed_at": "2025-11-06T15:00:00"
115+
"last_indexed_at": "2025-11-06T15:00:00",
116+
"indexing_stats": {
117+
"file_count": 150,
118+
"embedding_count": 450,
119+
"is_indexed": true
120+
}
116121
}
117122
```
118123

124+
**Indexing Stats Fields:**
125+
- `file_count`: Number of files indexed in the project
126+
- `embedding_count`: Number of embeddings (chunks) generated
127+
- `is_indexed`: Boolean indicating if project has any indexed files
128+
119129
### Delete Project
120130

121131
```http

endpoints/project_endpoints.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ def api_create_project(request: CreateProjectRequest):
4545
"""
4646
try:
4747
project = get_or_create_project(request.path, request.name)
48+
49+
# Add project to file watcher if available
50+
try:
51+
from main import _file_watcher
52+
if _file_watcher and _file_watcher.is_running():
53+
_file_watcher.add_project(project["id"], project["path"])
54+
except Exception as e:
55+
logger.warning(f"Could not add project to file watcher: {e}")
56+
4857
return JSONResponse(project)
4958
except ValueError as e:
5059
# ValueError is expected for invalid inputs, safe to show message
@@ -86,12 +95,38 @@ def api_get_project(project_id: str):
8695
8796
- **project_id**: Unique project identifier
8897
89-
Returns project metadata or 404 if not found.
98+
Returns project metadata including indexing status and statistics or 404 if not found.
9099
"""
91100
try:
92101
project = get_project_by_id(project_id)
93102
if not project:
94103
return JSONResponse({"error": "Project not found"}, status_code=404)
104+
105+
# Add indexing statistics if project has a database
106+
db_path = project.get("database_path")
107+
if db_path and os.path.exists(db_path):
108+
try:
109+
from db.operations import get_project_stats
110+
stats = get_project_stats(db_path)
111+
project["indexing_stats"] = {
112+
"file_count": stats.get("file_count", 0),
113+
"embedding_count": stats.get("embedding_count", 0),
114+
"is_indexed": stats.get("file_count", 0) > 0
115+
}
116+
except Exception as e:
117+
logger.warning(f"Could not get stats for project {project_id}: {e}")
118+
project["indexing_stats"] = {
119+
"file_count": 0,
120+
"embedding_count": 0,
121+
"is_indexed": False
122+
}
123+
else:
124+
project["indexing_stats"] = {
125+
"file_count": 0,
126+
"embedding_count": 0,
127+
"is_indexed": False
128+
}
129+
95130
return JSONResponse(project)
96131
except Exception as e:
97132
logger.exception(f"Error getting project: {e}")

endpoints/web_endpoints.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,26 @@ def api_health():
3333
- **status**: "ok" if service is running
3434
- **version**: API version
3535
- **features**: List of enabled features
36+
- **file_watcher**: Status of the FileWatcher (if enabled)
3637
3738
Use this endpoint for:
3839
- Load balancer health checks
3940
- Monitoring systems
4041
- Service availability verification
4142
"""
42-
return JSONResponse({
43+
from main import _file_watcher
44+
45+
health_data = {
4346
"status": "ok",
4447
"version": "0.2.0",
45-
"features": ["rag", "per-project-db", "pycharm-api", "incremental-indexing", "rate-limiting", "caching"]
46-
})
48+
"features": ["rag", "per-project-db", "pycharm-api", "incremental-indexing", "rate-limiting", "caching", "file-watcher"]
49+
}
50+
51+
# Add file watcher status if available
52+
if _file_watcher:
53+
health_data["file_watcher"] = _file_watcher.get_status()
54+
55+
return JSONResponse(health_data)
4756

4857

4958
@router.get("/", response_class=HTMLResponse)

main.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,26 @@
88
import os
99
import uvicorn
1010

11+
from db import operations as db_operations
1112
from db.operations import get_or_create_project
1213
from utils.config import CFG
1314
from utils.logger import get_logger
1415
from endpoints.project_endpoints import router as project_router
1516
from endpoints.query_endpoints import router as query_router
1617
from endpoints.web_endpoints import router as web_router
18+
from utils.file_watcher import FileWatcher
1719

1820
logger = get_logger(__name__)
1921

22+
# Global FileWatcher instance
23+
_file_watcher = None
24+
2025

2126
@asynccontextmanager
2227
async def lifespan(app: FastAPI):
2328
"""Application lifespan handler."""
29+
global _file_watcher
30+
2431
# Project registry is auto-initialized when needed via create_project
2532

2633
# Auto-create default project from configured local_path if it exists
@@ -31,7 +38,42 @@ async def lifespan(app: FastAPI):
3138
except Exception as e:
3239
logger.warning(f"Could not create default project: {e}")
3340

41+
# Start FileWatcher if enabled
42+
if CFG.get("file_watcher_enabled", True):
43+
try:
44+
_file_watcher = FileWatcher(
45+
logger=logger,
46+
enabled=True,
47+
debounce_seconds=CFG.get("file_watcher_debounce", 5),
48+
check_interval=CFG.get("file_watcher_interval", 10)
49+
)
50+
51+
# Add all existing projects to the watcher
52+
try:
53+
projects = db_operations.list_projects()
54+
for project in projects:
55+
if project.get("path") and os.path.exists(project["path"]):
56+
_file_watcher.add_project(project["id"], project["path"])
57+
except Exception as e:
58+
logger.warning(f"Could not add projects to file watcher: {e}")
59+
60+
_file_watcher.start()
61+
logger.info("FileWatcher started successfully")
62+
except Exception as e:
63+
logger.error(f"Failed to start FileWatcher: {e}")
64+
_file_watcher = None
65+
else:
66+
logger.info("FileWatcher is disabled in configuration")
67+
3468
yield
69+
70+
# Stop FileWatcher on shutdown
71+
if _file_watcher:
72+
try:
73+
_file_watcher.stop()
74+
logger.info("FileWatcher stopped successfully")
75+
except Exception as e:
76+
logger.error(f"Error stopping FileWatcher: {e}")
3577

3678

3779
app = FastAPI(

templates/index.html

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ <h5 class="card-title">Projects</h5>
6363
<div class="flex-grow-1">
6464
<div class="fw-bold">{{ p.name or p.path.split('/')[-1] }}</div>
6565
<small class="text-muted">{{ p.path }}</small><br>
66-
<small class="text-muted">Status: <span class="badge bg-{{ 'success' if p.status=='ready' else 'warning' }}">{{ p.status }}</span></small>
66+
<small class="text-muted">
67+
Status: <span class="badge bg-{{ 'success' if p.status=='ready' else ('warning' if p.status=='indexing' else 'secondary') }}" data-status="{{ p.status }}">{{ p.status }}</span>
68+
</small>
69+
<br>
70+
<small class="text-muted indexing-info" style="display:none;">
71+
Files: <span class="file-count">0</span> | Embeddings: <span class="embedding-count">0</span>
72+
</small>
6773
</div>
6874
<div>
69-
<form action="/projects/{{ p.id }}" method="post" style="display:inline;" onsubmit="return confirm('Delete this project and its database?');">
70-
<input type="hidden" name="_method" value="DELETE">
71-
<button type="submit" class="btn btn-sm btn-outline-danger">×</button>
72-
</form>
75+
<button type="button" class="btn btn-sm btn-outline-danger delete-project-btn" data-project-id="{{ p.id }}" title="Delete project">×</button>
7376
</div>
7477
</li>
7578
{% endfor %}
@@ -262,28 +265,81 @@ <h5 class="card-title">Chat</h5>
262265
firstProject.classList.add("active");
263266
}
264267

265-
// Poll for project status updates
268+
// Poll for project status updates with detailed stats
266269
setInterval(async () => {
267270
try {
268271
const response = await fetch("/projects/status");
269272
const projects = await response.json();
270273

271-
// Update project status badges
272-
projects.forEach(p => {
274+
// Update project status badges and stats
275+
for (const p of projects) {
273276
const item = document.querySelector(`[data-project-id="${p.id}"]`);
274277
if (item) {
275278
const badge = item.querySelector('.badge');
276279
if (badge) {
277280
badge.className = `badge bg-${p.status === 'ready' ? 'success' : p.status === 'indexing' ? 'warning' : 'secondary'}`;
278281
badge.textContent = p.status;
279282
}
283+
284+
// Fetch detailed stats for this project
285+
try {
286+
const detailResponse = await fetch(`/api/projects/${p.id}`);
287+
const details = await detailResponse.json();
288+
289+
if (details.indexing_stats) {
290+
const indexingInfo = item.querySelector('.indexing-info');
291+
const fileCount = item.querySelector('.file-count');
292+
const embeddingCount = item.querySelector('.embedding-count');
293+
294+
if (indexingInfo && fileCount && embeddingCount) {
295+
fileCount.textContent = details.indexing_stats.file_count || 0;
296+
embeddingCount.textContent = details.indexing_stats.embedding_count || 0;
297+
298+
// Show stats if project has been indexed
299+
if (details.indexing_stats.file_count > 0) {
300+
indexingInfo.style.display = 'block';
301+
} else {
302+
indexingInfo.style.display = 'none';
303+
}
304+
}
305+
}
306+
} catch (detailErr) {
307+
// Ignore errors fetching individual project details
308+
}
280309
}
281-
});
310+
}
282311
} catch (err) {
283312
console.error("Error polling status:", err);
284313
}
285314
}, 3000);
286315

316+
// Handle project deletion
317+
document.addEventListener('click', async (e) => {
318+
if (e.target.classList.contains('delete-project-btn')) {
319+
const projectId = e.target.getAttribute('data-project-id');
320+
321+
if (!confirm('Delete this project and its database?')) {
322+
return;
323+
}
324+
325+
try {
326+
const response = await fetch(`/projects/${projectId}`, {
327+
method: 'DELETE'
328+
});
329+
330+
if (response.ok) {
331+
// Reload page to show updated project list
332+
window.location.reload();
333+
} else {
334+
const data = await response.json();
335+
alert(`Failed to delete project: ${data.error || 'Unknown error'}`);
336+
}
337+
} catch (err) {
338+
alert(`Error deleting project: ${err.message}`);
339+
}
340+
}
341+
});
342+
287343
// Initial render
288344
renderChat();
289345
</script>

utils/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ def _bool_env(name, default):
3737
# uvicorn host/port (from .env)
3838
"uvicorn_host": os.getenv("UVICORN_HOST", "127.0.0.1"),
3939
"uvicorn_port": int(os.getenv("UVICORN_PORT", "8000")),
40+
41+
# FileWatcher configuration
42+
"file_watcher_enabled": _bool_env("FILE_WATCHER_ENABLED", True),
43+
"file_watcher_interval": _int_env("FILE_WATCHER_INTERVAL", 10),
44+
"file_watcher_debounce": _int_env("FILE_WATCHER_DEBOUNCE", 5),
4045
}

0 commit comments

Comments
 (0)