diff --git a/.claude/templates/coding_prompt.template.md b/.claude/templates/coding_prompt.template.md index 823d2972..f937abcd 100644 --- a/.claude/templates/coding_prompt.template.md +++ b/.claude/templates/coding_prompt.template.md @@ -22,15 +22,27 @@ tail -500 claude-progress.txt # 5. Check recent git history git log --oneline -20 + +# 6. Check for knowledge files (additional project context/requirements) +ls -la knowledge/ 2>/dev/null || echo "No knowledge directory" +``` + +**IMPORTANT:** If a `knowledge/` directory exists, read all `.md` files in it. +These contain additional project context, requirements documents, research notes, +or reference materials that will help you understand the project better. + +```bash +# Read all knowledge files if the directory exists +for f in knowledge/*.md; do [ -f "$f" ] && echo "=== $f ===" && cat "$f"; done 2>/dev/null ``` Then use MCP tools to check feature status: ``` -# 6. Get progress statistics (passing/total counts) +# 7. Get progress statistics (passing/total counts) Use the feature_get_stats tool -# 7. Get the next feature to work on +# 8. Get the next feature to work on Use the feature_get_next tool ``` diff --git a/.claude/templates/initializer_prompt.template.md b/.claude/templates/initializer_prompt.template.md index 312cd179..f24196c8 100644 --- a/.claude/templates/initializer_prompt.template.md +++ b/.claude/templates/initializer_prompt.template.md @@ -9,6 +9,25 @@ Start by reading `app_spec.txt` in your working directory. This file contains the complete specification for what you need to build. Read it carefully before proceeding. +### SECOND: Check for Knowledge Files + +Check if there's a `knowledge/` directory with additional context: + +```bash +ls -la knowledge/ 2>/dev/null || echo "No knowledge directory" +``` + +**IMPORTANT:** If a `knowledge/` directory exists, read ALL `.md` files in it. +These contain additional project context, requirements documents, research notes, +or reference materials that provide deeper understanding of the project. + +```bash +# Read all knowledge files if the directory exists +for f in knowledge/*.md; do [ -f "$f" ] && echo "=== $f ===" && cat "$f"; done 2>/dev/null +``` + +Use this information alongside `app_spec.txt` when creating features. + --- ## REQUIRED FEATURE COUNT diff --git a/server/routers/projects.py b/server/routers/projects.py index 68cf5268..4b00d107 100644 --- a/server/routers/projects.py +++ b/server/routers/projects.py @@ -14,6 +14,10 @@ from fastapi import APIRouter, HTTPException from ..schemas import ( + KnowledgeFile, + KnowledgeFileContent, + KnowledgeFileList, + KnowledgeFileUpload, ProjectCreate, ProjectDetail, ProjectPrompts, @@ -355,3 +359,136 @@ async def get_project_stats_endpoint(name: str): raise HTTPException(status_code=404, detail="Project directory not found") return get_project_stats(project_dir) + + +def get_knowledge_dir(project_dir: Path) -> Path: + """Get the knowledge directory for a project.""" + return project_dir / "knowledge" + + +@router.get("/{name}/knowledge", response_model=KnowledgeFileList) +async def list_knowledge_files(name: str): + """List all knowledge files for a project.""" + _init_imports() + _, _, get_project_path, _, _ = _get_registry_functions() + + name = validate_project_name(name) + project_dir = get_project_path(name) + + if not project_dir: + raise HTTPException(status_code=404, detail=f"Project '{name}' not found") + + if not project_dir.exists(): + raise HTTPException(status_code=404, detail="Project directory not found") + + knowledge_dir = get_knowledge_dir(project_dir) + + if not knowledge_dir.exists(): + return KnowledgeFileList(files=[], count=0) + + files = [] + for filepath in knowledge_dir.glob("*.md"): + if filepath.is_file(): + stat = filepath.stat() + from datetime import datetime + files.append(KnowledgeFile( + name=filepath.name, + size=stat.st_size, + modified=datetime.fromtimestamp(stat.st_mtime) + )) + + # Sort by name + files.sort(key=lambda f: f.name.lower()) + + return KnowledgeFileList(files=files, count=len(files)) + + +@router.get("/{name}/knowledge/{filename}", response_model=KnowledgeFileContent) +async def get_knowledge_file(name: str, filename: str): + """Get the content of a specific knowledge file.""" + _init_imports() + _, _, get_project_path, _, _ = _get_registry_functions() + + name = validate_project_name(name) + project_dir = get_project_path(name) + + if not project_dir: + raise HTTPException(status_code=404, detail=f"Project '{name}' not found") + + if not project_dir.exists(): + raise HTTPException(status_code=404, detail="Project directory not found") + + # Validate filename (prevent path traversal) + if not re.match(r'^[a-zA-Z0-9_\-\.]+\.md$', filename): + raise HTTPException(status_code=400, detail="Invalid filename") + + knowledge_dir = get_knowledge_dir(project_dir) + filepath = knowledge_dir / filename + + if not filepath.exists(): + raise HTTPException(status_code=404, detail=f"Knowledge file '{filename}' not found") + + try: + content = filepath.read_text(encoding="utf-8") + return KnowledgeFileContent(name=filename, content=content) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to read file: {e}") + + +@router.post("/{name}/knowledge", response_model=KnowledgeFileContent) +async def upload_knowledge_file(name: str, file: KnowledgeFileUpload): + """Upload a knowledge file to a project.""" + _init_imports() + _, _, get_project_path, _, _ = _get_registry_functions() + + name = validate_project_name(name) + project_dir = get_project_path(name) + + if not project_dir: + raise HTTPException(status_code=404, detail=f"Project '{name}' not found") + + if not project_dir.exists(): + raise HTTPException(status_code=404, detail="Project directory not found") + + knowledge_dir = get_knowledge_dir(project_dir) + knowledge_dir.mkdir(parents=True, exist_ok=True) + + filepath = knowledge_dir / file.filename + + try: + filepath.write_text(file.content, encoding="utf-8") + return KnowledgeFileContent(name=file.filename, content=file.content) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to write file: {e}") + + +@router.delete("/{name}/knowledge/{filename}") +async def delete_knowledge_file(name: str, filename: str): + """Delete a knowledge file from a project.""" + _init_imports() + _, _, get_project_path, _, _ = _get_registry_functions() + + name = validate_project_name(name) + project_dir = get_project_path(name) + + if not project_dir: + raise HTTPException(status_code=404, detail=f"Project '{name}' not found") + + if not project_dir.exists(): + raise HTTPException(status_code=404, detail="Project directory not found") + + # Validate filename (prevent path traversal) + if not re.match(r'^[a-zA-Z0-9_\-\.]+\.md$', filename): + raise HTTPException(status_code=400, detail="Invalid filename") + + knowledge_dir = get_knowledge_dir(project_dir) + filepath = knowledge_dir / filename + + if not filepath.exists(): + raise HTTPException(status_code=404, detail=f"Knowledge file '{filename}' not found") + + try: + filepath.unlink() + return {"success": True, "message": f"Deleted '{filename}'"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to delete file: {e}") diff --git a/server/schemas.py b/server/schemas.py index cb0a4ecc..3bc3796a 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -272,6 +272,35 @@ class CreateDirectoryRequest(BaseModel): name: str = Field(..., min_length=1, max_length=255) +# ============================================================================ +# Knowledge File Schemas +# ============================================================================ + +class KnowledgeFile(BaseModel): + """Information about a knowledge file.""" + name: str + size: int # Bytes + modified: datetime + + +class KnowledgeFileList(BaseModel): + """Response containing list of knowledge files.""" + files: list[KnowledgeFile] + count: int + + +class KnowledgeFileContent(BaseModel): + """Response containing knowledge file content.""" + name: str + content: str + + +class KnowledgeFileUpload(BaseModel): + """Request schema for uploading a knowledge file.""" + filename: str = Field(..., min_length=1, max_length=255, pattern=r'^[a-zA-Z0-9_\-\.]+\.md$') + content: str = Field(..., min_length=1) + + # ============================================================================ # Settings Schemas # ============================================================================ diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 328a31b7..2e5a98f6 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,153 +1,199 @@ -import { useState, useEffect, useCallback } from 'react' -import { useQueryClient } from '@tanstack/react-query' -import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects' -import { useProjectWebSocket } from './hooks/useWebSocket' -import { useFeatureSound } from './hooks/useFeatureSound' -import { useCelebration } from './hooks/useCelebration' - -const STORAGE_KEY = 'autocoder-selected-project' -import { ProjectSelector } from './components/ProjectSelector' -import { KanbanBoard } from './components/KanbanBoard' -import { AgentControl } from './components/AgentControl' -import { ProgressDashboard } from './components/ProgressDashboard' -import { SetupWizard } from './components/SetupWizard' -import { AddFeatureForm } from './components/AddFeatureForm' -import { FeatureModal } from './components/FeatureModal' -import { DebugLogViewer } from './components/DebugLogViewer' -import { AgentThought } from './components/AgentThought' -import { AssistantFAB } from './components/AssistantFAB' -import { AssistantPanel } from './components/AssistantPanel' -import { ExpandProjectModal } from './components/ExpandProjectModal' -import { SettingsModal } from './components/SettingsModal' -import { Loader2, Settings } from 'lucide-react' -import type { Feature } from './lib/types' +import { useState, useEffect, useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useProjects, useFeatures, useAgentStatus } from "./hooks/useProjects"; +import { useProjectWebSocket } from "./hooks/useWebSocket"; +import { useFeatureSound } from "./hooks/useFeatureSound"; +import { useCelebration } from "./hooks/useCelebration"; + +const STORAGE_KEY = "autocoder-selected-project"; +import { ProjectSelector } from "./components/ProjectSelector"; +import { KanbanBoard } from "./components/KanbanBoard"; +import { AgentControl } from "./components/AgentControl"; +import { ProgressDashboard } from "./components/ProgressDashboard"; +import { SetupWizard } from "./components/SetupWizard"; +import { AddFeatureForm } from "./components/AddFeatureForm"; +import { FeatureModal } from "./components/FeatureModal"; +import { DebugLogViewer } from "./components/DebugLogViewer"; +import { AgentThought } from "./components/AgentThought"; +import { AssistantFAB } from "./components/AssistantFAB"; +import { AssistantPanel } from "./components/AssistantPanel"; +import { ExpandProjectModal } from "./components/ExpandProjectModal"; +import { SettingsModal } from "./components/SettingsModal"; +import { KnowledgeFilesModal } from "./components/KnowledgeFilesModal"; +import { Loader2, Settings, BookOpen } from "lucide-react"; +import type { Feature } from "./lib/types"; function App() { // Initialize selected project from localStorage const [selectedProject, setSelectedProject] = useState(() => { try { - return localStorage.getItem(STORAGE_KEY) + return localStorage.getItem(STORAGE_KEY); } catch { - return null + return null; } - }) - const [showAddFeature, setShowAddFeature] = useState(false) - const [showExpandProject, setShowExpandProject] = useState(false) - const [selectedFeature, setSelectedFeature] = useState(null) - const [setupComplete, setSetupComplete] = useState(true) // Start optimistic - const [debugOpen, setDebugOpen] = useState(false) - const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height - const [assistantOpen, setAssistantOpen] = useState(false) - const [showSettings, setShowSettings] = useState(false) - const [isSpecCreating, setIsSpecCreating] = useState(false) - - const queryClient = useQueryClient() - const { data: projects, isLoading: projectsLoading } = useProjects() - const { data: features } = useFeatures(selectedProject) - useAgentStatus(selectedProject) // Keep polling for status updates - const wsState = useProjectWebSocket(selectedProject) + }); + const [showAddFeature, setShowAddFeature] = useState(false); + const [showExpandProject, setShowExpandProject] = useState(false); + const [selectedFeature, setSelectedFeature] = useState(null); + const [setupComplete, setSetupComplete] = useState(true); // Start optimistic + const [debugOpen, setDebugOpen] = useState(false); + const [debugPanelHeight, setDebugPanelHeight] = useState(288); // Default height + const [assistantOpen, setAssistantOpen] = useState(false); + const [showSettings, setShowSettings] = useState(false); + const [showKnowledgeFiles, setShowKnowledgeFiles] = useState(false); + const [isSpecCreating, setIsSpecCreating] = useState(false); + + const queryClient = useQueryClient(); + const { data: projects, isLoading: projectsLoading } = useProjects(); + const { data: features } = useFeatures(selectedProject); + useAgentStatus(selectedProject); // Keep polling for status updates + const wsState = useProjectWebSocket(selectedProject); // Play sounds when features move between columns - useFeatureSound(features) + useFeatureSound(features); // Celebrate when all features are complete - useCelebration(features, selectedProject) + useCelebration(features, selectedProject); // Persist selected project to localStorage const handleSelectProject = useCallback((project: string | null) => { - setSelectedProject(project) + setSelectedProject(project); try { if (project) { - localStorage.setItem(STORAGE_KEY, project) + localStorage.setItem(STORAGE_KEY, project); } else { - localStorage.removeItem(STORAGE_KEY) + localStorage.removeItem(STORAGE_KEY); } } catch { // localStorage not available } - }, []) + }, []); // Validate stored project exists (clear if project was deleted) useEffect(() => { - if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) { - handleSelectProject(null) + if ( + selectedProject && + projects && + !projects.some((p) => p.name === selectedProject) + ) { + handleSelectProject(null); } - }, [selectedProject, projects, handleSelectProject]) + }, [selectedProject, projects, handleSelectProject]); // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Ignore if user is typing in an input - if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { - return + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement + ) { + return; } // D : Toggle debug window - if (e.key === 'd' || e.key === 'D') { - e.preventDefault() - setDebugOpen(prev => !prev) + if (e.key === "d" || e.key === "D") { + e.preventDefault(); + setDebugOpen((prev) => !prev); } // N : Add new feature (when project selected) - if ((e.key === 'n' || e.key === 'N') && selectedProject) { - e.preventDefault() - setShowAddFeature(true) + if ((e.key === "n" || e.key === "N") && selectedProject) { + e.preventDefault(); + setShowAddFeature(true); } // E : Expand project with AI (when project selected and has features) - if ((e.key === 'e' || e.key === 'E') && selectedProject && features && - (features.pending.length + features.in_progress.length + features.done.length) > 0) { - e.preventDefault() - setShowExpandProject(true) + if ( + (e.key === "e" || e.key === "E") && + selectedProject && + features && + features.pending.length + + features.in_progress.length + + features.done.length > + 0 + ) { + e.preventDefault(); + setShowExpandProject(true); } // A : Toggle assistant panel (when project selected and not in spec creation) - if ((e.key === 'a' || e.key === 'A') && selectedProject && !isSpecCreating) { - e.preventDefault() - setAssistantOpen(prev => !prev) + if ( + (e.key === "a" || e.key === "A") && + selectedProject && + !isSpecCreating + ) { + e.preventDefault(); + setAssistantOpen((prev) => !prev); } // , : Open settings - if (e.key === ',') { - e.preventDefault() - setShowSettings(true) + if (e.key === ",") { + e.preventDefault(); + setShowSettings(true); + } + + // K : Open knowledge files (when project selected) + if ((e.key === "k" || e.key === "K") && selectedProject) { + e.preventDefault(); + setShowKnowledgeFiles(true); } // Escape : Close modals - if (e.key === 'Escape') { - if (showExpandProject) { - setShowExpandProject(false) + if (e.key === "Escape") { + if (showKnowledgeFiles) { + setShowKnowledgeFiles(false); + } else if (showExpandProject) { + setShowExpandProject(false); } else if (showSettings) { - setShowSettings(false) + setShowSettings(false); } else if (assistantOpen) { - setAssistantOpen(false) + setAssistantOpen(false); } else if (showAddFeature) { - setShowAddFeature(false) + setShowAddFeature(false); } else if (selectedFeature) { - setSelectedFeature(null) + setSelectedFeature(null); } else if (debugOpen) { - setDebugOpen(false) + setDebugOpen(false); } } - } - - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, assistantOpen, features, showSettings, isSpecCreating]) + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [ + selectedProject, + showAddFeature, + showExpandProject, + selectedFeature, + debugOpen, + assistantOpen, + features, + showSettings, + showKnowledgeFiles, + isSpecCreating, + ]); // Combine WebSocket progress with feature data - const progress = wsState.progress.total > 0 ? wsState.progress : { - passing: features?.done.length ?? 0, - total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0), - percentage: 0, - } + const progress = + wsState.progress.total > 0 + ? wsState.progress + : { + passing: features?.done.length ?? 0, + total: + (features?.pending.length ?? 0) + + (features?.in_progress.length ?? 0) + + (features?.done.length ?? 0), + percentage: 0, + }; if (progress.total > 0 && progress.percentage === 0) { - progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10 + progress.percentage = + Math.round((progress.passing / progress.total) * 100 * 10) / 10; } if (!setupComplete) { - return setSetupComplete(true)} /> + return setSetupComplete(true)} />; } return ( @@ -178,6 +224,15 @@ function App() { status={wsState.agentStatus} /> + + + + + + +

+ Upload markdown files with additional context, requirements, or + documentation for the agent. +

+ + {/* Loading State */} + {isLoading && ( +
+ + Loading files... +
+ )} + + {/* Error State */} + {isError && ( +
+
+ + Failed to load knowledge files +
+ +
+ )} + + {/* File List */} + {filesData && !isLoading && ( + <> + {filesData.files.length === 0 ? ( +
+ +

No knowledge files yet.

+

+ Create a new file to add context for the agent. +

+
+ ) : ( +
+ {filesData.files.map((file: KnowledgeFile) => ( +
+
+ +
+
{file.name}
+
+ {formatFileSize(file.size)} •{" "} + {formatDate(file.modified)} +
+
+
+
+ + + +
+
+ ))} +
+ )} + + )} + + {/* Delete Confirmation */} + {deleteConfirm && ( +
+
+

+ Delete File? +

+

+ Are you sure you want to delete {deleteConfirm}? + This action cannot be undone. +

+
+ + +
+
+
+ )} + + ); + + const renderView = () => ( + <> +
+
+ +

+ {selectedFile} +

+
+
+ + +
+
+ + {isLoadingContent ? ( +
+ +
+ ) : ( +
+
+            {fileContent?.content}
+          
+
+ )} + + ); + + const renderEdit = () => ( + <> +
+
+ +

+ {viewMode === "create" + ? "Create New File" + : `Edit: ${selectedFile}`} +

+
+ +
+ + {viewMode === "create" && ( +
+ + setNewFileName(e.target.value)} + placeholder="my-document.md" + className="neo-input w-full" + disabled={uploadFile.isPending} + /> +

+ .md extension will be added automatically if not provided +

+
+ )} + +
+ +