feat: Deep URL Navigation with Cross-Platform Copy Functionality#573
feat: Deep URL Navigation with Cross-Platform Copy Functionality#573stevepresley wants to merge 200 commits intocoleam00:mainfrom
Conversation
Implement comprehensive deep URL linking that allows direct navigation to:
- Specific projects: /projects/{projectId}
- Project tabs: /projects/{projectId}/docs or /projects/{projectId}/tasks
- Specific documents: /projects/{projectId}/docs/{documentId}
- Specific tasks: /projects/{projectId}/tasks/{taskId}
Key features:
- Full React Router v6 integration with URL parameters
- Automatic project/document/task selection from URLs
- Browser forward/back button support
- Bookmarkable and shareable URLs
- State preservation across page refreshes
- Graceful fallback for invalid IDs
Enhanced components:
- App.tsx: Added comprehensive route definitions
- ProjectPage.tsx: URL parameter handling and navigation logic
- DocsTab.tsx: Document selection from URL parameters
- TasksTab.tsx: Task selection support (interface prepared)
Documentation:
- Added comprehensive deep-url-linking.mdx guide
- Updated docs sidebar navigation
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Tasks from URLs now open in edit modal when accessed via /projects/{id}/tasks/{taskId}
- Modal closing clears task ID from URL
- URL updates when tasks are selected for editing
Ready for incremental testing in Docker.
This reverts commit 6a6b661.
Major performance improvements:
- Reduced API calls from 60+ to 3 for project/task navigation (96% reduction)
- Eliminated 31 individual archon_project_sources database calls
- Added lightweight background project loading with loading indicator
- Created optimized /projects/{id}/stats endpoint for task counts only
- Fixed Socket.IO broadcasts to use include_content=false
- Removed auto-reload behavior from health checks
- Added on-demand task count loading when switching projects
Technical changes:
- Backend: Updated Socket.IO handlers to skip expensive source linking
- Frontend: Implemented progressive loading (target project first, others lazily)
- Added loading card UI with skeleton placeholders for better UX
- Enhanced projectService.listProjects() to accept include_content parameter
- Fixed health check intervals to prevent overlapping timers
Performance impact:
- Page load time: ~3-5s → ~500ms for deep URLs
- Data transfer: ~400KB → ~15KB per navigation
- Backend load: Massive reduction in concurrent DB queries
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
Also addressed these suggestions that were not able to be shown in Diff view: Per guidelines, use database values directly: todo | doing | review | done. Update the Task.status union. interface Task {
saveDocument() references selectedDocument.tags/author. Add optional fields to the interface to satisfy type-checking. interface ProjectDoc {
bg-${color}-400 won’t be detected by the Tailwind scanner. Use predefined classes from your colorMap.
Add a dot entry per color: // outside selected range, update colorMap entries: Issue 1 - Status constants violate UI status guideline:
Issue 2 - Missing fields on ProjectDoc:
Issue 3 - Tailwind JIT dynamic class names:
All fixes maintain existing functionality while improving type safety and build Commit: edb0e52 |
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
@sean-eskerium, @Wirasm or @coleam00 Do I need to suggest/resolve the merge Conflicts, or do you all do that as part of the PR review/merge process? |
|
Also, I installed CodeRabbit on my personal repos (have them on my company repos), so I'll resolve the CodeRabbit suggestions locally next time before creating an upstream PR |
Thanks for this PR! Generally the expectation is for you to handle merge conflicts with your PR. I apologize for those happening since we are moving pretty quick right now as we make Archon super solid |
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
python/src/server/api_routes/projects_api.py (2)
70-78: Constrain task status types to DB enums.Prevent invalid writes; enforce allowed values in request models.
-from typing import Any +from typing import Any, Literal @@ class CreateTaskRequest(BaseModel): @@ - status: str | None = "todo" + status: Literal["todo", "doing", "review", "done"] | None = "todo" @@ class UpdateTaskRequest(BaseModel): @@ - status: str | None = None + status: Literal["todo", "doing", "review", "done"] | None = NoneAlso applies to: 844-851
703-717:create_taskignoresstatusfrom request.New tasks may be created with wrong/default status. Pass it through.
- success, result = await task_service.create_task( + success, result = await task_service.create_task( project_id=request.project_id, title=request.title, description=request.description or "", + status=request.status or "todo", assignee=request.assignee or "User", task_order=request.task_order or 0, feature=request.feature, )
♻️ Duplicate comments (3)
python/src/server/api_routes/projects_api.py (1)
646-652: Avoid transferring rows in count queries.Select only
idand addlimit(1); count is returned server-side.- todo_response = supabase_client.table("archon_tasks")\ - .select("*", count="exact")\ + todo_response = supabase_client.table("archon_tasks")\ + .select("id", count="exact")\ .eq("project_id", project_id)\ .eq("status", "todo")\ .or_("archived.is.null,archived.is.false")\ + .limit(1)\ .execute() @@ - doing_response = supabase_client.table("archon_tasks")\ - .select("*", count="exact")\ + doing_response = supabase_client.table("archon_tasks")\ + .select("id", count="exact")\ .eq("project_id", project_id)\ .in_("status", ["doing", "review"])\ .or_("archived.is.null,archived.is.false")\ + .limit(1)\ .execute() @@ - done_response = supabase_client.table("archon_tasks")\ - .select("*", count="exact")\ + done_response = supabase_client.table("archon_tasks")\ + .select("id", count="exact")\ .eq("project_id", project_id)\ .eq("status", "done")\ .or_("archived.is.null,archived.is.false")\ + .limit(1)\ .execute()Also applies to: 655-662, 664-670
archon-ui-main/src/utils/copyHelpers.ts (1)
20-31: Scopeconst viewinside the 'task' case to satisfy lints.Unscoped declarations in switch clauses violate noSwitchDeclarations and may leak.
- case 'task': - const view = currentView ? `?view=${currentView}` : ''; - return `${origin}/projects/${projectId}/tasks/${itemId}${view}`; + case 'task': { + const view = currentView ? `?view=${currentView}` : ''; + return `${origin}/projects/${projectId}/tasks/${itemId}${view}`; + }archon-ui-main/src/components/project-tasks/DocsTab.tsx (1)
1078-1082: Fix spread order so content cannot override title/type.Currently, content fields can overwrite the explicit title/document_type. Reverse the spread order.
- <PRPViewer - content={{ - title: selectedDocument.title, - document_type: selectedDocument.document_type, - ...selectedDocument.content - }} + <PRPViewer + content={{ + ...selectedDocument.content, + title: selectedDocument.title, + document_type: selectedDocument.document_type + }} />
🧹 Nitpick comments (25)
archon-ui-main/src/config/api.ts (2)
24-26: Reduce log noise in dev; silence in prod.Console logs here can be gated by a debug flag to avoid noisy output.
Apply:
- console.log('[Archon] getApiUrl(): browser dev →', url); + if (!import.meta.env.PROD && import.meta.env.VITE_DEBUG_API_URL === '1') { + console.log('[Archon] getApiUrl(): browser dev →', url); + }
29-40: Duplicate base-path logic; prefer single source of truth.getApiBasePath() and API_BASE_URL both represent '/api'. Keep API_BASE_URL and deprecate getApiBasePath() or have getApiBasePath() simply return API_BASE_URL for consistency.
Apply:
-export function getApiBasePath(): string { - const apiUrl = getApiUrl(); - - // If using relative URLs (empty string), just return /api - if (!apiUrl) { - return '/api'; - } - - // Otherwise, append /api to the base URL - return `${apiUrl}/api`; -} +export function getApiBasePath(): string { + return API_BASE_URL; +}Also applies to: 59-61
python/src/server/api_routes/socketio_handlers.py (4)
558-560: Catch asyncio.TimeoutError, not builtin TimeoutError.wait_for raises asyncio.TimeoutError.
Apply:
- except (TimeoutError, asyncio.CancelledError): + except (asyncio.TimeoutError, asyncio.CancelledError): pass
260-262: Duplicate disconnect logs.Two near-identical info logs; keep one.
Apply:
- logger.info(f"🔌 [SOCKETIO] Client disconnected: {sid}, was in rooms: {rooms}") - logger.info(f"Client disconnected: {sid}, was in rooms: {rooms}") + logger.info(f"🔌 [SOCKETIO] Client disconnected: {sid}, was in rooms: {rooms}")
103-105: Use logger.exception for stack traces.Prefer logger.exception(...) or exc_info=True to retain stack traces uniformly.
Example:
- except Exception as e: - logger.error(f"📄 [DOCUMENT SYNC] Error processing document batch: {e}") + except Exception: + logger.exception("📄 [DOCUMENT SYNC] Error processing document batch") await sio.emit("error", {"message": f"Failed to process batch: {str(e)}"}, to=sid)Also applies to: 769-772, 583-596
1066-1076: Cleanup task is defined but not started.Ensure start_document_sync_cleanup() is scheduled at app startup; otherwise locks may never expire.
Would you like a small init hook to spawn this via FastAPI lifespan?
Also applies to: 1079-1079
python/src/server/api_routes/projects_api.py (4)
143-151: Return an explicit 304 Response instead ofNone.Avoid returning
Nonefor cache hits; send a concrete Response.- if check_etag(if_none_match, current_etag): - response.status_code = http_status.HTTP_304_NOT_MODIFIED - response.headers["ETag"] = current_etag - response.headers["Cache-Control"] = "no-cache, must-revalidate" - return None + if check_etag(if_none_match, current_etag): + return Response( + status_code=http_status.HTTP_304_NOT_MODIFIED, + headers={ + "ETag": current_etag, + "Cache-Control": "no-cache, must-revalidate", + "Last-Modified": datetime.utcnow().isoformat(), + }, + )
599-606: Return an explicit 304 Response for tasks ETag path.Mirror the explicit 304 Response pattern here too.
- if check_etag(if_none_match, current_etag): - response.status_code = 304 - response.headers["ETag"] = current_etag - response.headers["Cache-Control"] = "no-cache, must-revalidate" - logfire.debug(f"Tasks unchanged, returning 304 | project_id={project_id} | etag={current_etag}") - return None + if check_etag(if_none_match, current_etag): + logfire.debug(f"Tasks unchanged, returning 304 | project_id={project_id} | etag={current_etag}") + return Response( + status_code=http_status.HTTP_304_NOT_MODIFIED, + headers={ + "ETag": current_etag, + "Cache-Control": "no-cache, must-revalidate", + "Last-Modified": datetime.utcnow().isoformat(), + }, + )
693-698: Include error context in 500 response.Return a typed payload with the exception message for observability.
- raise HTTPException(status_code=500, detail="Failed to retrieve project statistics") + raise HTTPException( + status_code=500, + detail={"error": "Failed to retrieve project statistics", "project_id": project_id, "reason": str(e)}, + )
237-247: Health check should avoid full scans.
list_tasks(include_closed=True)may fetch many rows; prefer a cheap existence probe (e.g., count 0/limit 1).archon-ui-main/src/utils/copyHelpers.ts (2)
56-75: Improve fallback copy UX on mobile.Prevent soft keyboard pop-up and minimize focus side-effects.
const input = document.createElement('input'); input.value = text; + input.setAttribute('readonly', ''); + input.setAttribute('aria-hidden', 'true');
89-99: DRY the 'typesRequiringItemId' constant.Declare once at module scope and reuse.
+const TYPES_REQUIRING_ITEM_ID: CopyButtonType[] = ['task', 'document']; @@ - const typesRequiringItemId: CopyButtonType[] = ['task', 'document']; - if (typesRequiringItemId.includes(type) && !itemId) { + if (TYPES_REQUIRING_ITEM_ID.includes(type) && !itemId) { @@ - const typesRequiringItemId: CopyButtonType[] = ['task', 'document']; - if (typesRequiringItemId.includes(type) && !itemId) { + if (TYPES_REQUIRING_ITEM_ID.includes(type) && !itemId) {Also applies to: 137-145
archon-ui-main/src/pages/ProjectPage.tsx (4)
3-3: Unused importuseSearchParams.Remove to keep the file lint-clean.
-import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; +import { useParams, useNavigate, useLocation } from 'react-router-dom';
40-51: Dead code:getProjectIconis not used.Drop it or wire it into the UI.
776-782: ResetnewProjectFormconsistently.You drop the
colorfield on reset; preserve default color to avoid undefined later.- setNewProjectForm({ title: '', description: '' }); + setNewProjectForm({ title: '', description: '', color: 'blue' }); @@ - setNewProjectForm({ title: '', description: '' }); + setNewProjectForm({ title: '', description: '', color: 'blue' });Also applies to: 818-821
1080-1117: Avoid direct DOMinnerHTMLmutation for copy feedback.Use React state to toggle a “Copied!” label/icon; direct mutation is brittle and can desync.
archon-ui-main/src/components/project-tasks/DocsTab.tsx (9)
2-2: Drop unused imports (lint).Eye and WebSocketState are imported but unused.
-import { Plus, X, Search, Upload, Link as LinkIcon, Check, Brain, Save, History, Eye, Edit3, Sparkles, Loader2 } from 'lucide-react'; +import { Plus, X, Search, Upload, Link as LinkIcon, Check, Brain, Save, History, Edit3, Sparkles, Loader2 } from 'lucide-react';-import { WebSocketState } from '../../services/socketIOService'; +// removed unused WebSocketStateAlso applies to: 12-12
597-599: Add actionable context to errors/toasts (projectId/docId).Surface IDs to ease debugging and align with logging guidelines.
- console.error('Failed to load documents:', error); - showToast('Failed to load documents', 'error'); + console.error('Failed to load documents', { projectId: project?.id, error }); + showToast(`Failed to load documents for project ${project?.id}`, 'error');- console.error('Failed to load full document:', error); - showToast('Failed to load document content', 'error'); + console.error('Failed to load full document', { projectId: project?.id, docId, error }); + showToast(`Failed to load document content (id: ${docId})`, 'error');Also applies to: 637-640
914-917: Safer error handling for WebSocket onError.Don’t assume Error type; normalize message before reporting.
- onError: (error) => { - console.error(`❌ WebSocket error for ${progressId}:`, error); - handleProgressError(`Connection error: ${error.message}`); - } + onError: (error) => { + console.error(`❌ WebSocket error for ${progressId}:`, error); + const msg = error instanceof Error ? error.message : String(error); + handleProgressError(`Connection error: ${msg}`); + }
1044-1047: Micro: simplify selection after delete.updatedDocuments already excludes docId; no need for an extra find.
- if (selectedDocument?.id === docId) { - setSelectedDocument(updatedDocuments.find(d => d.id !== docId) || null); - } + if (selectedDocument?.id === docId) { + setSelectedDocument(updatedDocuments[0] || null); + }
721-745: Use service/hook instead of raw fetch for project data; add retry/backoff.Swap the ad-hoc fetch with projectService (or usePolling/useProjectMutation) to centralize retries, error mapping, and typing.
Do you want me to refactor this to a useProject hook with exponential backoff and typed response?
747-758: React Hooks deps: stabilize loaders or include them in deps.loadKnowledgeItems/loadProjectDocuments/loadProjectData are re-created every render but omitted from the deps. Either wrap them with useCallback([project?.id]) or include them to satisfy react-hooks/exhaustive-deps and prevent accidental staleness.
778-784: Add loadFullDocument to the callback deps (after stabilizing it).Wrap loadFullDocument in useCallback first, then include it here to avoid stale closure.
979-991: Save button visibility: isEditing is never set true.Either wire isEditing via editor callbacks or remove the dead UI. As-is, the Save button likely never shows.
936-1150: Guard rendering that depends on project.id.Minor robustness: don’t render DocumentCard list when project is null to avoid future refactors reintroducing a null access.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
.gitignore(1 hunks)archon-ui-main/src/App.tsx(2 hunks)archon-ui-main/src/components/project-tasks/DocsTab.tsx(1 hunks)archon-ui-main/src/config/api.ts(2 hunks)archon-ui-main/src/index.css(2 hunks)archon-ui-main/src/pages/ProjectPage.tsx(1 hunks)archon-ui-main/src/services/serverHealthService.ts(3 hunks)archon-ui-main/src/utils/copyHelpers.ts(1 hunks)python/src/server/api_routes/projects_api.py(2 hunks)python/src/server/api_routes/socketio_handlers.py(1 hunks)python/src/server/services/projects/project_service.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- .gitignore
- archon-ui-main/src/App.tsx
- archon-ui-main/src/index.css
- archon-ui-main/src/services/serverHealthService.ts
🧰 Additional context used
📓 Path-based instructions (8)
python/src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
python/src/**/*.py: Fail fast on service startup failures, missing configuration, database connection failures, authentication/authorization failures, critical dependencies unavailable, and invalid/corrupting data
Never accept or store corrupted data; on operation failure skip the item entirely rather than writing placeholders (e.g., zero embeddings)
For batch/background operations, continue processing but log each failure with details; track both successes and failures
Use specific exception types (avoid bare Exception), include context/IDs/URLs in messages, preserve full stack traces with logging (exc_info=True), and never return None/null to indicate failure—raise with details
Use database task status values directly: todo, doing, review, done
Target Python 3.12 style with 120-character line length; use Ruff for linting and Mypy for type checking
python/src/**/*.py: Target Python 3.12 and keep lines within 120 characters
Use Ruff for linting (errors, warnings, unused imports, code style) and keep code Ruff-clean
Use Mypy for type checking; maintain type-safe code across backend
Files:
python/src/server/services/projects/project_service.pypython/src/server/api_routes/projects_api.pypython/src/server/api_routes/socketio_handlers.py
python/src/server/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Use FastAPI exception handlers to return rich error responses with appropriate HTTP status codes and typed error payloads
Files:
python/src/server/services/projects/project_service.pypython/src/server/api_routes/projects_api.pypython/src/server/api_routes/socketio_handlers.py
python/src/server/services/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Backend service method naming mirrors CRUD patterns: get/create/update/delete with clear resource scoping
Files:
python/src/server/services/projects/project_service.py
python/src/{server,agents,mcp}/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
python/src/{server,agents,mcp}/**/*.py: Fail fast on service startup failures (credentials, DB, or service init) with clear errors
Treat missing configuration (env vars/invalid settings) as fatal and stop execution
Do not suppress database connection failures; bubble up immediately
Authentication/authorization failures must halt the operation and be clearly surfaced
Data corruption or validation errors should raise (use Pydantic to enforce), never silently accept
If a critical dependency is unavailable, fail immediately
Never persist invalid data that would corrupt state (e.g., zero embeddings, null FKs, malformed JSON)
Batch processing should continue, logging detailed errors per item; always return both successes and failures
Background tasks (e.g., embedding generation) should finish queues and log failures without crashing the whole process
Include context and IDs/URLs in error messages; preserve full stack traces with logging (exc_info=True)
Use specific exception types; avoid catching bare Exception
Never return None to signal failure; raise exceptions with details
For crawling/document processing: continue batches, skip failed items entirely, and log detailed errors; never store placeholders (e.g., zeroed embeddings)
Files:
python/src/server/services/projects/project_service.pypython/src/server/api_routes/projects_api.pypython/src/server/api_routes/socketio_handlers.py
archon-ui-main/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use state naming conventions: is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
archon-ui-main/src/**/*.{ts,tsx}: WebSocket event failures should be logged and not crash serving other clients
External API calls should retry with exponential backoff and ultimately fail with a clear, specific message
Include actionable context in frontend error logs/messages (what was attempted, relevant IDs/URLs)
Never return null to signal failure in async/data flows; throw errors with details
Use polling (HTTP) with provided hooks (usePolling, useDatabaseMutation, useProjectMutation); Socket.IO is removed
State naming: is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
Persist theme choice in localStorage and respect Tailwind dark mode classes across components
Files:
archon-ui-main/src/utils/copyHelpers.tsarchon-ui-main/src/pages/ProjectPage.tsxarchon-ui-main/src/components/project-tasks/DocsTab.tsxarchon-ui-main/src/config/api.ts
archon-ui-main/src/**/*.{ts,tsx,py}
📄 CodeRabbit inference engine (AGENTS.md)
Avoid comment keywords like LEGACY, CHANGED, REMOVED; write comments that document current functionality only
Files:
archon-ui-main/src/utils/copyHelpers.tsarchon-ui-main/src/pages/ProjectPage.tsxarchon-ui-main/src/components/project-tasks/DocsTab.tsxarchon-ui-main/src/config/api.ts
archon-ui-main/src/components/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use ESLint (standard React rules) for legacy UI code in /src/components
Files:
archon-ui-main/src/components/project-tasks/DocsTab.tsx
archon-ui-main/src/components/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Legacy components live under src/components and may be used by pages; add related tests under archon-ui-main/test
Files:
archon-ui-main/src/components/project-tasks/DocsTab.tsx
🧠 Learnings (9)
📚 Learning: 2025-09-06T20:05:27.061Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-06T20:05:27.061Z
Learning: Applies to archon-ui-main/src/**/*.{ts,tsx} : Include actionable context in frontend error logs/messages (what was attempted, relevant IDs/URLs)
Applied to files:
archon-ui-main/src/utils/copyHelpers.ts
📚 Learning: 2025-09-06T20:05:27.061Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-06T20:05:27.061Z
Learning: Applies to archon-ui-main/src/services/**/*.ts : Use GET /api/projects/{id}/tasks (not getTasks) for project tasks
Applied to files:
archon-ui-main/src/utils/copyHelpers.tsarchon-ui-main/src/pages/ProjectPage.tsxpython/src/server/api_routes/projects_api.py
📚 Learning: 2025-08-28T12:56:47.840Z
Learnt from: Wirasm
PR: coleam00/Archon#514
File: archon-ui-main/src/pages/ProjectPage.tsx:329-331
Timestamp: 2025-08-28T12:56:47.840Z
Learning: In the ProjectPage.tsx polling refactor, temporary project creation logic with progress cards was removed in favor of simpler modal loading states, as the complexity of managing in-flight temporary projects wasn't justified when HTTP polling would show new projects within seconds anyway.
Applied to files:
archon-ui-main/src/pages/ProjectPage.tsx
📚 Learning: 2025-09-06T20:05:27.061Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-06T20:05:27.061Z
Learning: Applies to archon-ui-main/src/components/**/*.{ts,tsx} : Legacy components live under src/components and may be used by pages; add related tests under archon-ui-main/test
Applied to files:
archon-ui-main/src/pages/ProjectPage.tsx
📚 Learning: 2025-09-06T20:05:27.061Z
Learnt from: CR
PR: coleam00/Archon#0
File: AGENTS.md:0-0
Timestamp: 2025-09-06T20:05:27.061Z
Learning: Applies to archon-ui-main/src/features/**/*.{ts,tsx} : No prop drilling; use TanStack Query and context/hooks for state/data flow
Applied to files:
archon-ui-main/src/components/project-tasks/DocsTab.tsx
📚 Learning: 2025-09-06T20:04:08.111Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-06T20:04:08.111Z
Learning: Applies to archon-ui-main/src/features/**/tasks/**/*.{ts,tsx} : Use task status values directly from the database: todo, doing, review, done (no UI mapping)
Applied to files:
python/src/server/api_routes/projects_api.py
📚 Learning: 2025-09-04T18:53:35.857Z
Learnt from: stevepresley
PR: coleam00/Archon#573
File: python/src/server/api_routes/projects_api.py:0-0
Timestamp: 2025-09-04T18:53:35.857Z
Learning: Server-side counting optimization pattern: When counting rows by status/category in Supabase, use separate queries with count="exact" parameter instead of fetching all rows. Example: supabase_client.table("table").select("id", count="exact").eq("field", "value").execute() and read res.count. This reduces complexity from O(n) to O(1) and eliminates unnecessary data transfer.
Applied to files:
python/src/server/api_routes/projects_api.py
📚 Learning: 2025-09-06T20:04:08.111Z
Learnt from: CR
PR: coleam00/Archon#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-06T20:04:08.111Z
Learning: Applies to python/src/**/*.py : Use database task status values directly: todo, doing, review, done
Applied to files:
python/src/server/api_routes/projects_api.py
📚 Learning: 2025-09-04T16:30:05.187Z
Learnt from: stevepresley
PR: coleam00/Archon#573
File: archon-ui-main/src/config/api.ts:15-25
Timestamp: 2025-09-04T16:30:05.187Z
Learning: Archon UI API config: Prefer lazy getters getApiFullUrl() and getWsUrl() over module-load constants to avoid SSR/test crashes. Avoid CommonJS exports patterns (Object.defineProperty(exports,…)) in ESM. Add typeof window guards with VITE_API_URL fallback inside getApiUrl()/getWebSocketUrl() when SSR safety is required.
Applied to files:
archon-ui-main/src/config/api.ts
🔇 Additional comments (6)
archon-ui-main/src/config/api.ts (3)
11-18: Good SSR guard in getApiUrl().The non-browser env fallback prevents SSR/test crashes.
44-57: Frontend guideline mismatch: reintroduces WebSocket usage.Guidelines for archon-ui-main specify polling hooks and “Socket.IO is removed.” Confirm that Socket.IO usage on the UI is intended and guidelines are updated accordingly.
Run:
#!/bin/bash # Inspect repo for WebSocket usage on the frontend rg -n --type=ts --type=tsx -g 'archon-ui-main/src/**' '\b(io|socket\.io|getWsUrl|new WebSocket)\b' -C2
62-68: Verify call-sites don’t rely on API_FULL_URL constant.Ensure consumers use getApiFullUrl()/getWsUrl() and not API_FULL_URL.
Run:
#!/bin/bash rg -n 'API_FULL_URL|WS_URL|\bgetApiFullUrl\b|\bgetWsUrl\b' -g '!**/dist/**' -C1Also applies to: 72-74
python/src/server/services/projects/project_service.py (1)
137-138: LGTM: UI-safe lightweight payload fields added.Adding empty data/features arrays preserves shape without payload bloat.
archon-ui-main/src/pages/ProjectPage.tsx (1)
258-309: ```shell
#!/bin/bashVerify Socket.IO usage in ProjectPage.tsx
rg -n "projectListSocketIO" .
Confirm presence of usePolling hook in codebase
rg -n "usePolling" --type=ts --type=tsx .
</blockquote></details> <details> <summary>archon-ui-main/src/components/project-tasks/DocsTab.tsx (1)</summary><blockquote> `504-519`: **Persist view mode (?view) to match deep-link behavior.** PR notes say view mode persists via ?view. This component manages viewMode locally; ensure it is synced with URL or handled by the parent. ```shell #!/bin/bash # Find where ?view is read/written to confirm persistence is implemented at page/router level. rg -nP -C3 --type=ts --type=tsx '\bsearchParams\.get\(["'\''"]view|setSearchParams\(|\?view=|viewMode'Also applies to: 766-775
| const [selectedDocument, setSelectedDocument] = useState<ProjectDoc | null>(null); | ||
| const [isEditing, setIsEditing] = useState(false); | ||
| const [isSaving, setIsSaving] = useState(false); | ||
| const [loading, setLoading] = useState(false); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Split loading state: separate isLoadingDocs and isLoadingKnowledge (aligns with naming guidelines and prevents UI flicker).
A single “loading” drives both document cards/content and knowledge fetches, causing “Loading documents…” to appear during knowledge requests. Use dedicated states and is[Action]ing naming.
- const [loading, setLoading] = useState(false);
+ const [isLoadingDocs, setIsLoadingDocs] = useState(false);
+ const [isLoadingKnowledge, setIsLoadingKnowledge] = useState(false);- setLoading(true);
+ setIsLoadingDocs(true);- setDocuments([]);
- setLoading(false);
+ setDocuments([]);
+ setIsLoadingDocs(false);
return;- } finally {
- setLoading(false);
- }
+ } finally {
+ setIsLoadingDocs(false);
+ }- setLoading(true);
+ setIsLoadingKnowledge(true);- } finally {
- setLoading(false);
- }
+ } finally {
+ setIsLoadingKnowledge(false);
+ }- {loading ? (
+ {isLoadingDocs ? (- {loading ? (
+ {isLoadingDocs ? (Also applies to: 571-573, 579-581, 597-601, 789-803, 1009-1028, 1069-1073
🤖 Prompt for AI Agents
In archon-ui-main/src/components/project-tasks/DocsTab.tsx around lines 525,
571-573, 579-581, 597-601, 789-803, 1009-1028, and 1069-1073 the component uses
a single loading state which drives both document UI and knowledge fetches
causing incorrect "Loading documents..." flicker; introduce two boolean states
(isLoadingDocs and isLoadingKnowledge) replacing usages of loading so that
document-card/content render and labels use isLoadingDocs while knowledge
fetches and their spinners use isLoadingKnowledge, set each state only around
the corresponding async calls (e.g., setIsLoadingDocs(true)/false around doc
fetches and setIsLoadingKnowledge(true)/false around knowledge fetches), update
any conditional rendering, labels, and effects to reference the new names, and
remove/replace the original loading state variable and its setters.
| try { | ||
| setIsSaving(true); | ||
|
|
||
| // Create the document in the database first | ||
| const newDocument = await projectService.createDocument(project.id, { | ||
| title: template.name, | ||
| content: template.content, | ||
| document_type: template.document_type, | ||
| tags: [] | ||
| }); | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Template dates are frozen at module-load; generate fresh dates at creation time.
The template objects compute dates once at import. Ensure each new document gets “today’s” date.
- // Create the document in the database first
- const newDocument = await projectService.createDocument(project.id, {
- title: template.name,
- content: template.content,
- document_type: template.document_type,
- tags: []
- });
+ // Create the document in the database first
+ const contentWithDynamicDates = (() => {
+ const c = JSON.parse(JSON.stringify(template.content));
+ if (c && typeof c === 'object') {
+ if ('date' in c) c.date = new Date().toISOString().split('T')[0];
+ if ('meeting_date' in c) c.meeting_date = new Date().toISOString().split('T')[0];
+ }
+ return c;
+ })();
+
+ const newDocument = await projectService.createDocument(project!.id, {
+ title: template.name,
+ content: contentWithDynamicDates,
+ document_type: template.document_type,
+ tags: []
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| setIsSaving(true); | |
| // Create the document in the database first | |
| const newDocument = await projectService.createDocument(project.id, { | |
| title: template.name, | |
| content: template.content, | |
| document_type: template.document_type, | |
| tags: [] | |
| }); | |
| try { | |
| setIsSaving(true); | |
| // Create the document in the database first | |
| const contentWithDynamicDates = (() => { | |
| const c = JSON.parse(JSON.stringify(template.content)); | |
| if (c && typeof c === 'object') { | |
| if ('date' in c) c.date = new Date().toISOString().split('T')[0]; | |
| if ('meeting_date' in c) c.meeting_date = new Date().toISOString().split('T')[0]; | |
| } | |
| return c; | |
| })(); | |
| const newDocument = await projectService.createDocument(project!.id, { | |
| title: template.name, | |
| content: contentWithDynamicDates, | |
| document_type: template.document_type, | |
| tags: [] | |
| }); | |
| // …rest of existing logic… |
🤖 Prompt for AI Agents
In archon-ui-main/src/components/project-tasks/DocsTab.tsx around lines 650 to
660, the template objects use module-load timestamps so new documents inherit
stale dates; ensure you generate fresh date values at creation time by replacing
any use of template.created_at/updated_at (or similar date fields) with new
Date() (or Date.now()/ISO string as the app expects) right before calling
projectService.createDocument so each created document gets today's/current
timestamps.
| try { | ||
| // Call API to delete from database first | ||
| await projectService.deleteDocument(project.id, docId); | ||
|
|
||
| // Then remove from local state and handle selection atomically | ||
| setDocuments(prev => { | ||
| const updatedDocuments = prev.filter(d => d.id !== docId); | ||
| // If the deleted document was selected, update selection using the new list | ||
| if (selectedDocument?.id === docId) { | ||
| setSelectedDocument(updatedDocuments.find(d => d.id !== docId) || null); | ||
| } | ||
| return updatedDocuments; | ||
| }); | ||
| showToast('Document deleted', 'success'); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Type-safety: guard or assert project.id in callbacks/props.
project is optional; using project.id can trip TS “possibly undefined.” Use a guard or a non-null assertion where we’re already operating in a project context.
- await projectService.deleteDocument(project.id, docId);
+ await projectService.deleteDocument(project!.id, docId);- projectId={project.id}
+ projectId={project!.id}Also applies to: 1057-1058
🤖 Prompt for AI Agents
In archon-ui-main/src/components/project-tasks/DocsTab.tsx around lines 1037 to
1050 (and likewise at 1057-1058), the code uses project.id directly which can be
undefined; guard or assert the project id before using it: ensure you either
early-return when project or project.id is falsy or capture a non-null id at the
start of the callback (e.g., const projectId = project?.id; if (!projectId)
return) or use a local non-null assertion when you know the context guarantees a
project (const projectId = project!.id), then replace project.id usages with
projectId so TypeScript no longer reports “possibly undefined.”
| <<<<<<< HEAD | ||
| // Get WebSocket URL for real-time connections | ||
| export function getWebSocketUrl(): string { | ||
| const apiUrl = getApiUrl(); | ||
|
|
||
| // If using relative URLs, construct from current location (handles PROD or empty env) | ||
| if (!apiUrl) { | ||
| if (typeof window === 'undefined') return ''; // SSR/tests: return empty to avoid crashes | ||
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | ||
| const host = window.location.host; | ||
| return `${protocol}//${host}`; | ||
| } | ||
|
|
||
| // Convert http/https to ws/wss | ||
| return apiUrl.replace(/^http/, 'ws'); | ||
| } | ||
|
|
||
| // Export commonly used values | ||
| export const API_BASE_URL = '/api'; // Always use relative URL for API calls | ||
|
|
||
| // Lazy-evaluated exports to avoid calling getApiUrl/getWebSocketUrl at module load time | ||
| let _apiFullUrl: string | null = null; | ||
| let _wsUrl: string | null = null; | ||
|
|
||
| export const getApiFullUrl = (): string => (_apiFullUrl ??= getApiUrl()); | ||
| export const getWsUrl = (): string => (_wsUrl ??= getWebSocketUrl()); | ||
|
|
||
| // Remove legacy CommonJS-style defineProperty exports — not needed and unsafe in ESM | ||
| ======= | ||
| // Export commonly used values | ||
| export const API_BASE_URL = '/api'; // Always use relative URL for API calls | ||
| export const API_FULL_URL = getApiUrl(); | ||
| >>>>>>> upstream/main |
There was a problem hiding this comment.
Unresolved merge conflict — build blocker.
Conflict markers present; also reintroduces module-load evaluation of API_FULL_URL. Resolve by keeping lazy getters and removing static constant.
Apply:
-<<<<<<< HEAD
// Get WebSocket URL for real-time connections
export function getWebSocketUrl(): string {
const apiUrl = getApiUrl();
// If using relative URLs, construct from current location (handles PROD or empty env)
if (!apiUrl) {
if (typeof window === 'undefined') return ''; // SSR/tests: return empty to avoid crashes
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
return `${protocol}//${host}`;
}
// Convert http/https to ws/wss
return apiUrl.replace(/^http/, 'ws');
}
// Export commonly used values
export const API_BASE_URL = '/api'; // Always use relative URL for API calls
// Lazy-evaluated exports to avoid calling getApiUrl/getWebSocketUrl at module load time
let _apiFullUrl: string | null = null;
let _wsUrl: string | null = null;
export const getApiFullUrl = (): string => (_apiFullUrl ??= getApiUrl());
export const getWsUrl = (): string => (_wsUrl ??= getWebSocketUrl());
// Remove legacy CommonJS-style defineProperty exports — not needed and unsafe in ESM
-=======
-// Export commonly used values
-export const API_BASE_URL = '/api'; // Always use relative URL for API calls
-export const API_FULL_URL = getApiUrl();
->>>>>>> upstream/main📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <<<<<<< HEAD | |
| // Get WebSocket URL for real-time connections | |
| export function getWebSocketUrl(): string { | |
| const apiUrl = getApiUrl(); | |
| // If using relative URLs, construct from current location (handles PROD or empty env) | |
| if (!apiUrl) { | |
| if (typeof window === 'undefined') return ''; // SSR/tests: return empty to avoid crashes | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.host; | |
| return `${protocol}//${host}`; | |
| } | |
| // Convert http/https to ws/wss | |
| return apiUrl.replace(/^http/, 'ws'); | |
| } | |
| // Export commonly used values | |
| export const API_BASE_URL = '/api'; // Always use relative URL for API calls | |
| // Lazy-evaluated exports to avoid calling getApiUrl/getWebSocketUrl at module load time | |
| let _apiFullUrl: string | null = null; | |
| let _wsUrl: string | null = null; | |
| export const getApiFullUrl = (): string => (_apiFullUrl ??= getApiUrl()); | |
| export const getWsUrl = (): string => (_wsUrl ??= getWebSocketUrl()); | |
| // Remove legacy CommonJS-style defineProperty exports — not needed and unsafe in ESM | |
| ======= | |
| // Export commonly used values | |
| export const API_BASE_URL = '/api'; // Always use relative URL for API calls | |
| export const API_FULL_URL = getApiUrl(); | |
| >>>>>>> upstream/main | |
| // Get WebSocket URL for real-time connections | |
| export function getWebSocketUrl(): string { | |
| const apiUrl = getApiUrl(); | |
| // If using relative URLs, construct from current location (handles PROD or empty env) | |
| if (!apiUrl) { | |
| if (typeof window === 'undefined') return ''; // SSR/tests: return empty to avoid crashes | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.host; | |
| return `${protocol}//${host}`; | |
| } | |
| // Convert http/https to ws/wss | |
| return apiUrl.replace(/^http/, 'ws'); | |
| } | |
| // Export commonly used values | |
| export const API_BASE_URL = '/api'; // Always use relative URL for API calls | |
| // Lazy-evaluated exports to avoid calling getApiUrl/getWebSocketUrl at module load time | |
| let _apiFullUrl: string | null = null; | |
| let _wsUrl: string | null = null; | |
| export const getApiFullUrl = (): string => (_apiFullUrl ??= getApiUrl()); | |
| export const getWsUrl = (): string => (_wsUrl ??= getWebSocketUrl()); | |
| // Remove legacy CommonJS-style defineProperty exports — not needed and unsafe in ESM |
🤖 Prompt for AI Agents
In archon-ui-main/src/config/api.ts around lines 42 to 74, remove the unresolved
git conflict markers and the duplicate/static API_FULL_URL export; restore and
keep the lazy-evaluated implementations from the HEAD side (getWebSocketUrl,
API_BASE_URL, _apiFullUrl/_wsUrl and the getApiFullUrl/getWsUrl getters) so
module-load time does not call getApiUrl/getWebSocketUrl, and delete the
upstream/main change that introduced export const API_FULL_URL = getApiUrl().
| <<<<<<< HEAD | ||
| import { useState, useEffect, useCallback } from 'react'; | ||
| import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; | ||
| import { useToast } from '../contexts/ToastContext'; | ||
| import { motion, AnimatePresence } from 'framer-motion'; | ||
| import { useStaggeredEntrance } from '../hooks/useStaggeredEntrance'; | ||
| import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/project-tasks/Tabs'; | ||
| import { DocsTab } from '../components/project-tasks/DocsTab'; | ||
| // import { FeaturesTab } from '../components/project-tasks/FeaturesTab'; | ||
| // import { DataTab } from '../components/project-tasks/DataTab'; | ||
| import { TasksTab } from '../components/project-tasks/TasksTab'; | ||
| import { Button } from '../components/ui/Button'; | ||
| import { ChevronRight, ShoppingCart, Code, Briefcase, Layers, Plus, X, AlertCircle, Loader2, Heart, BarChart3, Trash2, Pin, ListTodo, Activity, CheckCircle2, Clipboard, ExternalLink } from 'lucide-react'; | ||
|
|
||
| // Import our service layer and types | ||
| import { projectService } from '../services/projectService'; | ||
| import type { Project, CreateProjectRequest } from '../types/project'; | ||
| import type { Task } from '../components/project-tasks/TaskTableView'; | ||
| import { ProjectCreationProgressCard } from '../components/ProjectCreationProgressCard'; | ||
| import { projectCreationProgressService } from '../services/projectCreationProgressService'; | ||
| import type { ProjectCreationProgressData } from '../services/projectCreationProgressService'; | ||
|
|
||
| // Import copy utilities | ||
| import { handleCopyClick, copyUrlToClipboard } from '../utils/copyHelpers'; | ||
| import { needsCopyLinkButton } from '../utils/platformDetection'; | ||
| import { projectListSocketIO, taskUpdateSocketIO } from '../services/socketIOService'; | ||
| ======= | ||
| import { ProjectsViewWithBoundary } from '../features/projects'; | ||
|
|
||
| // Minimal wrapper for routing compatibility | ||
| // All implementation is in features/projects/views/ProjectsView.tsx | ||
| // Uses ProjectsViewWithBoundary for proper error handling | ||
| >>>>>>> upstream/main | ||
|
|
There was a problem hiding this comment.
Unresolved merge conflict and duplicate ProjectPage definitions.
The file contains conflict markers and two competing implementations; build will fail. Resolve by choosing one approach and deleting the other.
Two quick options (pick one):
Option A — keep wrapper (aligns with features-based structure):
-<<<<<<< HEAD
-...huge page implementation...
-=======
import { ProjectsViewWithBoundary } from '../features/projects';
// Minimal wrapper for routing compatibility
// All implementation is in features/projects/views/ProjectsView.tsx
// Uses ProjectsViewWithBoundary for proper error handling
-
-function ProjectPage(props: any) {
- return <ProjectsViewWithBoundary {...props} />;
-}
-...
-export { ProjectPage };
->>>>>>> upstream/main
+export function ProjectPage(props: any) {
+ return <ProjectsViewWithBoundary {...props} />;
+}
+export { ProjectPage };Option B — keep the full page here: remove the wrapper import/usage and all conflict markers, and ensure types are defined.
Also applies to: 1461-1463
🤖 Prompt for AI Agents
In archon-ui-main/src/pages/ProjectPage.tsx around lines 1 to 34 (also note
related conflict area at lines 1461-1463), the file contains unresolved git
conflict markers and two competing implementations of ProjectPage causing
duplicate definitions and build failure; choose one approach and remove the
other plus all conflict markers: either keep the minimal wrapper import/export
that uses ProjectsViewWithBoundary (delete the full-page implementation and any
unused imports/types), or keep the full ProjectPage implementation (remove the
wrapper import/usage and ensure all referenced types/imports are present), then
run TypeScript/ESLint to fix any remaining unused-import or type errors and
commit the cleaned file.
| const tasksData = await projectService.getTasksByProject(projectId); | ||
|
|
||
| // Convert backend tasks to UI format | ||
| const uiTasks: Task[] = tasksData.map(task => ({ | ||
| id: task.id, | ||
| title: task.title, | ||
| description: task.description, | ||
| status: (task.uiStatus || 'backlog') as Task['status'], | ||
| assignee: { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Status mapping uses uiStatus and defaults to 'backlog'.
Align with canonical statuses: todo, doing, review, done. Use task.status and default to 'todo'.
- status: (task.uiStatus || 'backlog') as Task['status'],
+ status: (task.status || 'todo') as Task['status'],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const tasksData = await projectService.getTasksByProject(projectId); | |
| // Convert backend tasks to UI format | |
| const uiTasks: Task[] = tasksData.map(task => ({ | |
| id: task.id, | |
| title: task.title, | |
| description: task.description, | |
| status: (task.uiStatus || 'backlog') as Task['status'], | |
| assignee: { | |
| const tasksData = await projectService.getTasksByProject(projectId); | |
| // Convert backend tasks to UI format | |
| const uiTasks: Task[] = tasksData.map(task => ({ | |
| id: task.id, | |
| title: task.title, | |
| description: task.description, | |
| status: (task.status || 'todo') as Task['status'], | |
| assignee: { |
🤖 Prompt for AI Agents
In archon-ui-main/src/pages/ProjectPage.tsx around lines 568 to 576, the code
maps backend task status using task.uiStatus and defaults to 'backlog'; update
this to use task.status and default to 'todo', ensuring the value is one of the
canonical statuses (todo, doing, review, done) before casting to Task['status']
— implement a small guard or mapping function that normalizes/validates
task.status and falls back to 'todo' for unknown/null values.
| <<<<<<< HEAD | ||
| from fastapi import APIRouter, HTTPException, Depends | ||
| ======= | ||
| from fastapi import APIRouter, Header, HTTPException, Request, Response | ||
| from fastapi import status as http_status | ||
| >>>>>>> upstream/main |
There was a problem hiding this comment.
Resolve merge conflicts and fix missing imports.
Unresolved conflict markers will break imports and http_status is referenced later but not imported. Choose one side and ensure all required symbols are imported.
Apply:
-<<<<<<< HEAD
-from fastapi import APIRouter, HTTPException, Depends
-=======
-from fastapi import APIRouter, Header, HTTPException, Request, Response
-from fastapi import status as http_status
->>>>>>> upstream/main
+from fastapi import APIRouter, Header, HTTPException, Request, Response
+from fastapi import status as http_status📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <<<<<<< HEAD | |
| from fastapi import APIRouter, HTTPException, Depends | |
| ======= | |
| from fastapi import APIRouter, Header, HTTPException, Request, Response | |
| from fastapi import status as http_status | |
| >>>>>>> upstream/main | |
| from fastapi import APIRouter, Header, HTTPException, Request, Response | |
| from fastapi import status as http_status |
🤖 Prompt for AI Agents
In python/src/server/api_routes/projects_api.py around lines 15 to 20, there are
unresolved Git conflict markers and missing imports: remove the conflict markers
and merge the imports so the file imports all required symbols (APIRouter,
HTTPException, Depends if used elsewhere in this module) plus Header, Request,
Response and "status as http_status" so references to http_status compile;
ensure the final import line is a single consolidated from fastapi import ...
statement containing the union of required names and delete the leftover
conflict markers.
| async def join_project(sid, data): | ||
| """Join a project room to receive task updates.""" | ||
| project_id = data.get("project_id") | ||
| if not project_id: | ||
| await sio.emit("error", {"message": "project_id required"}, to=sid) | ||
| return | ||
|
|
||
| # Join the room for this project | ||
| await sio.enter_room(sid, project_id) | ||
| logger.info(f"📥 [SOCKETIO] Client {sid} joined project room: {project_id}") | ||
| logger.info(f"Client {sid} joined project {project_id}") | ||
|
|
||
| # Send confirmation - let frontend request initial tasks via API | ||
| await sio.emit("joined_project", {"project_id": project_id}, to=sid) | ||
|
|
There was a problem hiding this comment.
No authentication/authorization checks on mutating events.
join_project, document_change, lock_document/unlock_document, and delete_document accept client-supplied IDs without verifying user/session authorization. High risk of unauthorized edits/deletes.
Minimal hardening sketch (adjust to your auth layer):
+def _require_user(sid, data) -> str:
+ user_id = (data or {}).get("user_id")
+ if not user_id:
+ raise PermissionError("user_id required")
+ # TODO: verify sid ↔ user_id binding via your session/auth provider
+ return user_idUse at handler starts:
-async def document_change(sid, data):
+async def document_change(sid, data):
+ try:
+ _require_user(sid, data)
+ except PermissionError as e:
+ await sio.emit("error", {"message": str(e)}, to=sid); returnRepeat for lock/unlock/delete/join_project. Also validate project membership.
Also applies to: 699-713, 873-981, 986-1019
🤖 Prompt for AI Agents
In python/src/server/api_routes/socketio_handlers.py around lines 265 to 279,
the join_project handler currently accepts client-supplied project_id without
verifying the caller; add authentication and authorization checks at the top of
the handler: verify the session/user identity for sid (e.g., map sid to
authenticated user or validate token in data), ensure the user is a member of
the requested project (or has required role/permission), return a clear error
and do not call sio.enter_room if unauthorized, and validate that project_id
exists. Apply the same pattern (authenticate user/session, validate
project/document existence, and authorize membership/permissions) to the other
mutating handlers mentioned (lines 699-713, 873-981, 986-1019).
| # In-memory document state storage (in production, use Redis or database) | ||
| document_states: dict[str, DocumentState] = {} | ||
| document_locks: dict[str, dict[str, Any]] = {} | ||
|
|
||
|
|
||
| @sio.event | ||
| async def join_document_room(sid, data): | ||
| """Join a document room for real-time collaboration.""" | ||
| project_id = data.get("project_id") | ||
| document_id = data.get("document_id") | ||
|
|
||
| if not project_id or not document_id: | ||
| await sio.emit("error", {"message": "project_id and document_id required"}, to=sid) | ||
| return | ||
|
|
||
| room_name = f"doc_{project_id}_{document_id}" | ||
| await sio.enter_room(sid, room_name) | ||
|
|
||
| logger.info(f"📄 [DOCUMENT SYNC] Client {sid} joined document room: {room_name}") | ||
|
|
||
| # Send current document state if exists | ||
| if document_id in document_states: | ||
| state = document_states[document_id] | ||
| await sio.emit("document_state", asdict(state), to=sid) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Document state keyed only by document_id — collision across projects.
document_states and document_locks use document_id as key, but rooms use project+document. Use a composite key consistently.
Apply pattern:
-# In-memory document state storage
-document_states: dict[str, DocumentState] = {}
-document_locks: dict[str, dict[str, Any]] = {}
+document_states: dict[str, DocumentState] = {}
+document_locks: dict[str, dict[str, Any]] = {}
+
+def _doc_key(project_id: str, document_id: str) -> str:
+ return f"{project_id}:{document_id}"Join/send:
- if document_id in document_states:
- state = document_states[document_id]
+ key = _doc_key(project_id, document_id)
+ if key in document_states:
+ state = document_states[key]Process/apply:
- if document_id not in document_states:
- document_states[document_id] = DocumentState(
+ key = _doc_key(project_id, document_id)
+ if key not in document_states:
+ document_states[key] = DocumentState(
id=document_id,
project_id=project_id,
...
)
- state = document_states[document_id]
+ state = document_states[key]Locks:
- if document_id in document_locks:
- existing_lock = document_locks[document_id]
+ key = _doc_key(project_id, document_id)
+ if key in document_locks:
+ existing_lock = document_locks[key]
...
- document_locks[document_id] = {"user_id": user_id, "expiry": lock_expiry, "sid": sid}
+ document_locks[key] = {"user_id": user_id, "expiry": lock_expiry, "sid": sid}Delete/cleanup similarly switch to composite key.
Also applies to: 774-796, 903-915, 956-964, 998-1004, 1025-1063
🤖 Prompt for AI Agents
In python/src/server/api_routes/socketio_handlers.py around lines 637-660 (and
also adjust at 774-796, 903-915, 956-964, 998-1004, 1025-1063), document_states
and document_locks are keyed only by document_id while rooms use a composite
project+document key, causing collisions across projects; update all usages to
compute a composite key (e.g., f"{project_id}:{document_id}") when storing,
retrieving, locking, and deleting document state and locks, ensure join/send
handlers use the same composite key before accessing
document_states/document_locks, update lock acquisition/release and any cleanup
to reference the composite key, and keep room naming unchanged so socket rooms
still use project+document but backend state consistently uses the composite
key.
| # Apply the change | ||
| if change.change_type == "content": | ||
| if isinstance(change.data, dict) and isinstance(state.content, dict): | ||
| state.content.update(change.data) | ||
| else: | ||
| state.content = change.data | ||
| elif change.change_type == "title": | ||
| state.title = change.data.get("title", state.title) | ||
| elif change.change_type == "metadata": | ||
| if isinstance(change.data, dict) and isinstance(state.metadata, dict): | ||
| state.metadata.update(change.data) | ||
| else: | ||
| state.metadata = change.data | ||
| elif change.change_type == "delete": | ||
| # Mark for deletion - in practice, you might want to soft delete | ||
| state.metadata["deleted"] = True | ||
| state.metadata["deleted_by"] = change.user_id | ||
| state.metadata["deleted_at"] = change.timestamp | ||
|
|
||
| # Update state metadata | ||
| state.version = max(state.version, change.version) | ||
| state.last_modified = change.timestamp | ||
| state.last_modified_by = change.user_id | ||
|
|
||
| # Broadcast change to other clients in the room if enabled | ||
| if broadcast: | ||
| room_name = f"doc_{project_id}_{document_id}" | ||
|
|
||
| event_data = { | ||
| "type": "document_updated", | ||
| "document_id": document_id, | ||
| "project_id": project_id, | ||
| "user_id": change.user_id, | ||
| "timestamp": change.timestamp, | ||
| "data": change.data, | ||
| "version": state.version, | ||
| "change_type": change.change_type, | ||
| } | ||
|
|
||
| await sio.emit("document_updated", event_data, room=room_name, skip_sid=sid) | ||
| logger.info(f"📄 [DOCUMENT SYNC] Broadcasted {change.change_type} change for {document_id}") | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Locks aren’t enforced during edits.
process_document_change applies changes even when another user holds a valid lock. Enforce lock ownership/expiry before applying changes.
Apply:
+ # Enforce locks: reject edits by non-owners while lock is active
+ key = _doc_key(project_id, document_id)
+ lock = document_locks.get(key)
+ now_ms = time.time() * 1000
+ if lock and lock["expiry"] > now_ms and lock["user_id"] != change.user_id:
+ return {
+ "type": "locked",
+ "document_id": document_id,
+ "locked_by": lock["user_id"],
+ "resolution": "rejected",
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Apply the change | |
| if change.change_type == "content": | |
| if isinstance(change.data, dict) and isinstance(state.content, dict): | |
| state.content.update(change.data) | |
| else: | |
| state.content = change.data | |
| elif change.change_type == "title": | |
| state.title = change.data.get("title", state.title) | |
| elif change.change_type == "metadata": | |
| if isinstance(change.data, dict) and isinstance(state.metadata, dict): | |
| state.metadata.update(change.data) | |
| else: | |
| state.metadata = change.data | |
| elif change.change_type == "delete": | |
| # Mark for deletion - in practice, you might want to soft delete | |
| state.metadata["deleted"] = True | |
| state.metadata["deleted_by"] = change.user_id | |
| state.metadata["deleted_at"] = change.timestamp | |
| # Update state metadata | |
| state.version = max(state.version, change.version) | |
| state.last_modified = change.timestamp | |
| state.last_modified_by = change.user_id | |
| # Broadcast change to other clients in the room if enabled | |
| if broadcast: | |
| room_name = f"doc_{project_id}_{document_id}" | |
| event_data = { | |
| "type": "document_updated", | |
| "document_id": document_id, | |
| "project_id": project_id, | |
| "user_id": change.user_id, | |
| "timestamp": change.timestamp, | |
| "data": change.data, | |
| "version": state.version, | |
| "change_type": change.change_type, | |
| } | |
| await sio.emit("document_updated", event_data, room=room_name, skip_sid=sid) | |
| logger.info(f"📄 [DOCUMENT SYNC] Broadcasted {change.change_type} change for {document_id}") | |
| # Enforce locks: reject edits by non-owners while lock is active | |
| key = _doc_key(project_id, document_id) | |
| lock = document_locks.get(key) | |
| now_ms = time.time() * 1000 | |
| if lock and lock["expiry"] > now_ms and lock["user_id"] != change.user_id: | |
| return { | |
| "type": "locked", | |
| "document_id": document_id, | |
| "locked_by": lock["user_id"], | |
| "resolution": "rejected", | |
| } | |
| # Apply the change | |
| if change.change_type == "content": | |
| if isinstance(change.data, dict) and isinstance(state.content, dict): | |
| state.content.update(change.data) | |
| else: | |
| state.content = change.data | |
| elif change.change_type == "title": | |
| state.title = change.data.get("title", state.title) | |
| elif change.change_type == "metadata": | |
| if isinstance(change.data, dict) and isinstance(state.metadata, dict): | |
| state.metadata.update(change.data) | |
| else: | |
| state.metadata = change.data | |
| elif change.change_type == "delete": | |
| # Mark for deletion - in practice, you might want to soft delete | |
| state.metadata["deleted"] = True | |
| state.metadata["deleted_by"] = change.user_id | |
| state.metadata["deleted_at"] = change.timestamp | |
| # Update state metadata | |
| state.version = max(state.version, change.version) | |
| state.last_modified = change.timestamp | |
| state.last_modified_by = change.user_id | |
| # Broadcast change to other clients in the room if enabled | |
| if broadcast: | |
| room_name = f"doc_{project_id}_{document_id}" | |
| event_data = { | |
| "type": "document_updated", | |
| "document_id": document_id, | |
| "project_id": project_id, | |
| "user_id": change.user_id, | |
| "timestamp": change.timestamp, | |
| "data": change.data, | |
| "version": state.version, | |
| "change_type": change.change_type, | |
| } | |
| await sio.emit("document_updated", event_data, room=room_name, skip_sid=sid) | |
| logger.info(f"📄 [DOCUMENT SYNC] Broadcasted {change.change_type} change for {document_id}") |
🤖 Prompt for AI Agents
In python/src/server/api_routes/socketio_handlers.py around lines 827 to 868,
the handler currently applies changes without checking document locks; you must
enforce lock ownership/expiry before applying any change: first retrieve the
document's lock info from state.metadata (e.g., lock_owner and lock_expires_at),
if a lock exists and is not expired and lock_owner != change.user_id then
abort/return an error (or emit a "lock_conflict" event) instead of applying
updates; if the lock exists but has expired, clear the lock fields and proceed;
if no lock or owned by the requester, continue to apply the change and then
update state metadata/version/timestamps as before; ensure broadcast behavior
respects the abort path so no change is emitted when rejected.
|
I think I'm going to redo this PR with the new branch. The last agent made commits with the conflict markers and broke the whole thing. |
…573) Both the emitter event and the DB event were calling Date.now() - dagStartTime independently, resulting in two slightly different duration values for the same workflow completion due to async I/O between the two calls. Changes: - Extract `const duration = Date.now() - dagStartTime` before both usages - Replace inline computations in emitter.emit() and store.createWorkflowEvent() with the single `duration` variable Fixes #570
…00#570) (coleam00#573) Both the emitter event and the DB event were calling Date.now() - dagStartTime independently, resulting in two slightly different duration values for the same workflow completion due to async I/O between the two calls. Changes: - Extract `const duration = Date.now() - dagStartTime` before both usages - Replace inline computations in emitter.emit() and store.createWorkflowEvent() with the single `duration` variable Fixes coleam00#570
…00#570) (coleam00#573) Both the emitter event and the DB event were calling Date.now() - dagStartTime independently, resulting in two slightly different duration values for the same workflow completion due to async I/O between the two calls. Changes: - Extract `const duration = Date.now() - dagStartTime` before both usages - Replace inline computations in emitter.emit() and store.createWorkflowEvent() with the single `duration` variable Fixes coleam00#570
Summary
Implementation of deep URL navigation for projects, tasks, and documents with comprehensive cross-platform copy functionality, mobile support, performance optimizations, and scrollbar fixes for all three areas. Also allows direct navigation to Table or Board views in the Tasks area of a project.
✅ Deep URL Navigation Features
/projects/{projectId}- Auto-loads and highlights specific projects/projects/{projectId}/tasks?view=table/board- Navigate directly to Board or Table view for a project's task list/projects/{projectId}/tasks/{taskId}?view=table/board- Task highlighting with view persistence/projects/{projectId}/docs/{docId}- Document loading and display?view=table/boardfor shareable, bookmarkable URLs✅ Cross-Platform Copy Functionality
✅ Task Highlighting & Navigation
✅ Performance Optimizations
✅ Cross-Platform Scrollbar Fixes
force-scrollbarCSS class with-webkit-appearance: noneProjectPage.tsx)DocsTab.tsx)src/index.css,ProjectPage.tsx,DocsTab.tsx, Task table components✅ Mobile UX Fixes
projectId is not definederror in TaskBoardView✅ Additional Bug Fixes
window.location.hostnameinstead of hardcoded 'localhost'✅ Cross-Platform Testing
📋 Technical Implementation
platformDetection.ts,copyHelpers.tsfor cross-platform supportreference, and UI components documentation
🔗 Example URLs
http://localhost:3737/projects/452536cf-1fd2-45dc-82cb-064c79f487f8http://localhost:3737/projects/452536cf-1fd2-45dc-82cb-064c79f487f8/tasks?view=tablehttp://localhost:3737/projects/452536cf-1fd2-45dc-82cb-064c79f487f8/tasks?view=boardhttp://localhost:3737/projects/452536cf-1fd2-45dc-82cb-064c79f487f8/tasks/8a93d601-ec86-486a-9fef-b487a1b375fe?view=tablehttp://localhost:3737/projects/452536cf-1fd2-45dc-82cb-064c79f487f8/tasks/8a93d601-ec86-486a-9fef-b487a1b375fe?view=boardhttp://localhost:3737/projects/452536cf-1fd2-45dc-82cb-064c79f487f8/docs/bc6df4e7-5fe5-480c-8769-5717c8f4960bTest Plan
Implementation Details
Frontend Routing
Uses React Router v6 with enhanced route definitions:
Route path="/projects/:projectId/tasks/:taskId"for task deep linkingRoute path="/projects/:projectId/docs/:documentId"for document deep linkinguseParams()and navigation withuseNavigate()URL State Management
/docsand/tasksdetermine active tabreplace: truefor clean browser historyError Handling
Summary by CodeRabbit
New Features
UI/UX
Performance
Bug Fixes
Documentation