From f68600bb76a566a725ed4ebeab7bdd5ad43932e5 Mon Sep 17 00:00:00 2001
From: Haileyesus
Date: Mon, 2 Mar 2026 17:04:02 +0300
Subject: [PATCH] refactor(prd-editor): modularize PRD editor with typed
feature modules
Break the legacy PRDEditor.jsx monolith into a feature-based TypeScript architecture under src/components/prd-editor while keeping behavior parity and readability.
Key changes:
- Replace PRDEditor.jsx with a typed orchestrator component and a compatibility export bridge at src/components/PRDEditor.tsx.
- Split responsibilities into dedicated hooks: document loading/init, existing PRD registry fetching, save workflow with overwrite detection, and keyboard shortcuts.
- Split UI into focused view components: header, editor/preview body, footer stats, loading state, generate-tasks modal, and overwrite-confirm modal.
- Move filename concerns into utility helpers (sanitize, extension handling, default naming) and centralize template/constants.
- Keep component-local state close to the UI that owns it (workspace controls/modal toggles), while shared workflow state remains in the feature container.
- Reuse the existing MarkdownPreview component for safer markdown rendering instead of ad-hoc HTML conversion.
- Update TaskMasterPanel integration to consume typed PRDEditor directly (remove any-cast) and pass isExisting metadata for correct overwrite behavior.
- Keep all new/changed files below 300 lines and add targeted comments where behavior needs clarification.
Validation:
- npm run typecheck
- npm run build
---
src/components/PRDEditor.jsx | 871 ------------------
src/components/PRDEditor.tsx | 1 +
.../view/subcomponents/TaskMasterPanel.tsx | 4 +-
src/components/prd-editor/PRDEditor.tsx | 138 +++
src/components/prd-editor/constants.ts | 67 ++
.../prd-editor/hooks/usePrdDocument.ts | 132 +++
.../hooks/usePrdKeyboardShortcuts.ts | 34 +
.../prd-editor/hooks/usePrdRegistry.ts | 50 +
src/components/prd-editor/hooks/usePrdSave.ts | 111 +++
src/components/prd-editor/index.ts | 1 +
src/components/prd-editor/types.ts | 30 +
src/components/prd-editor/utils/fileName.ts | 18 +
.../prd-editor/view/GenerateTasksModal.tsx | 77 ++
.../prd-editor/view/OverwriteConfirmModal.tsx | 60 ++
.../prd-editor/view/PrdEditorBody.tsx | 61 ++
.../prd-editor/view/PrdEditorFooter.tsx | 36 +
.../prd-editor/view/PrdEditorHeader.tsx | 228 +++++
.../prd-editor/view/PrdEditorLoadingState.tsx | 12 +
.../prd-editor/view/PrdEditorWorkspace.tsx | 114 +++
19 files changed, 1172 insertions(+), 873 deletions(-)
delete mode 100644 src/components/PRDEditor.jsx
create mode 100644 src/components/PRDEditor.tsx
create mode 100644 src/components/prd-editor/PRDEditor.tsx
create mode 100644 src/components/prd-editor/constants.ts
create mode 100644 src/components/prd-editor/hooks/usePrdDocument.ts
create mode 100644 src/components/prd-editor/hooks/usePrdKeyboardShortcuts.ts
create mode 100644 src/components/prd-editor/hooks/usePrdRegistry.ts
create mode 100644 src/components/prd-editor/hooks/usePrdSave.ts
create mode 100644 src/components/prd-editor/index.ts
create mode 100644 src/components/prd-editor/types.ts
create mode 100644 src/components/prd-editor/utils/fileName.ts
create mode 100644 src/components/prd-editor/view/GenerateTasksModal.tsx
create mode 100644 src/components/prd-editor/view/OverwriteConfirmModal.tsx
create mode 100644 src/components/prd-editor/view/PrdEditorBody.tsx
create mode 100644 src/components/prd-editor/view/PrdEditorFooter.tsx
create mode 100644 src/components/prd-editor/view/PrdEditorHeader.tsx
create mode 100644 src/components/prd-editor/view/PrdEditorLoadingState.tsx
create mode 100644 src/components/prd-editor/view/PrdEditorWorkspace.tsx
diff --git a/src/components/PRDEditor.jsx b/src/components/PRDEditor.jsx
deleted file mode 100644
index 3f1ff358..00000000
--- a/src/components/PRDEditor.jsx
+++ /dev/null
@@ -1,871 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import CodeMirror from '@uiw/react-codemirror';
-import { markdown } from '@codemirror/lang-markdown';
-import { oneDark } from '@codemirror/theme-one-dark';
-import { EditorView } from '@codemirror/view';
-import { X, Save, Download, Maximize2, Minimize2, Eye, FileText, Sparkles, AlertTriangle } from 'lucide-react';
-import { cn } from '../lib/utils';
-import { api, authenticatedFetch } from '../utils/api';
-
-const PRDEditor = ({
- file,
- onClose,
- projectPath,
- project, // Add project object
- initialContent = '',
- isNewFile = false,
- onSave
-}) => {
- const [content, setContent] = useState(initialContent);
- const [loading, setLoading] = useState(!isNewFile);
- const [saving, setSaving] = useState(false);
- const [isFullscreen, setIsFullscreen] = useState(false);
- const [isDarkMode, setIsDarkMode] = useState(true);
- const [saveSuccess, setSaveSuccess] = useState(false);
- const [previewMode, setPreviewMode] = useState(false);
- const [wordWrap, setWordWrap] = useState(true); // Default to true for markdown
- const [fileName, setFileName] = useState('');
- const [showGenerateModal, setShowGenerateModal] = useState(false);
- const [showOverwriteConfirm, setShowOverwriteConfirm] = useState(false);
- const [existingPRDs, setExistingPRDs] = useState([]);
-
- const editorRef = useRef(null);
-
- const PRD_TEMPLATE = `# Product Requirements Document - Example Project
-
-## 1. Overview
-**Product Name:** AI-Powered Task Manager
-**Version:** 1.0
-**Date:** 2024-12-27
-**Author:** Development Team
-
-This document outlines the requirements for building an AI-powered task management application that integrates with development workflows and provides intelligent task breakdown and prioritization.
-
-## 2. Objectives
-- Create an intuitive task management system that works seamlessly with developer tools
-- Provide AI-powered task generation from high-level requirements
-- Enable real-time collaboration and progress tracking
-- Integrate with popular development environments (VS Code, Cursor, etc.)
-
-### Success Metrics
-- User adoption rate > 80% within development teams
-- Task completion rate improvement of 25%
-- Time-to-delivery reduction of 15%
-
-## 3. User Stories
-
-### Core Functionality
-- As a project manager, I want to create PRDs that automatically generate detailed tasks so I can save time on project planning
-- As a developer, I want to see my next task clearly highlighted so I can maintain focus
-- As a team lead, I want to track progress across multiple projects so I can provide accurate status updates
-- As a developer, I want tasks to be broken down into implementable subtasks so I can work more efficiently
-
-### AI Integration
-- As a user, I want to describe a feature in natural language and get detailed implementation tasks so I can start working immediately
-- As a project manager, I want the AI to analyze task complexity and suggest appropriate time estimates
-- As a developer, I want intelligent task prioritization based on dependencies and deadlines
-
-### Collaboration
-- As a team member, I want to see real-time updates when tasks are completed so I can coordinate my work
-- As a stakeholder, I want to view project progress through intuitive dashboards
-- As a developer, I want to add implementation notes to tasks for future reference
-
-## 4. Functional Requirements
-
-### Task Management
-- Create, edit, and delete tasks with rich metadata (priority, status, dependencies, estimates)
-- Hierarchical task structure with subtasks and sub-subtasks
-- Real-time status updates and progress tracking
-- Dependency management with circular dependency detection
-- Bulk operations (move, update status, assign)
-
-### AI Features
-- Natural language PRD parsing to generate structured tasks
-- Intelligent task breakdown with complexity analysis
-- Automated subtask generation with implementation details
-- Smart dependency suggestion
-- Progress prediction based on historical data
-
-### Integration Features
-- VS Code/Cursor extension for in-editor task management
-- Git integration for linking commits to tasks
-- API for third-party tool integration
-- Webhook support for external notifications
-- CLI tool for command-line task management
-
-### User Interface
-- Responsive web application (desktop and mobile)
-- Multiple view modes (Kanban, list, calendar)
-- Dark/light theme support
-- Drag-and-drop task organization
-- Advanced filtering and search capabilities
-- Keyboard shortcuts for power users
-
-## 5. Technical Requirements
-
-### Frontend
-- React.js with TypeScript for type safety
-- Modern UI framework (Tailwind CSS)
-- State management (Context API or Redux)
-- Real-time updates via WebSockets
-- Progressive Web App (PWA) support
-- Accessibility compliance (WCAG 2.1 AA)
-
-### Backend
-- Node.js with Express.js framework
-- RESTful API design with OpenAPI documentation
-- Real-time communication via Socket.io
-- Background job processing
-- Rate limiting and security middleware
-
-### AI Integration
-- Integration with multiple AI providers (OpenAI, Anthropic, etc.)
-- Fallback model support
-- Context-aware prompt engineering
-- Token usage optimization
-- Model response caching
-
-### Database
-- Primary: PostgreSQL for relational data
-- Cache: Redis for session management and real-time features
-- Full-text search capabilities
-- Database migrations and seeding
-- Backup and recovery procedures
-
-### Infrastructure
-- Docker containerization
-- Cloud deployment (AWS/GCP/Azure)
-- Auto-scaling capabilities
-- Monitoring and logging (structured logging)
-- CI/CD pipeline with automated testing
-
-## 6. Non-Functional Requirements
-
-### Performance
-- Page load time < 2 seconds
-- API response time < 500ms for 95% of requests
-- Support for 1000+ concurrent users
-- Efficient handling of large task lists (10,000+ tasks)
-
-### Security
-- JWT-based authentication with refresh tokens
-- Role-based access control (RBAC)
-- Data encryption at rest and in transit
-- Regular security audits and penetration testing
-- GDPR and privacy compliance
-
-### Reliability
-- 99.9% uptime SLA
-- Graceful error handling and recovery
-- Data backup every 6 hours with point-in-time recovery
-- Disaster recovery plan with RTO < 4 hours
-
-### Scalability
-- Horizontal scaling for both frontend and backend
-- Database read replicas for query optimization
-- CDN for static asset delivery
-- Microservices architecture for future expansion
-
-## 7. User Experience Design
-
-### Information Architecture
-- Intuitive navigation with breadcrumbs
-- Context-aware menus and actions
-- Progressive disclosure of complex features
-- Consistent design patterns throughout
-
-### Interaction Design
-- Smooth animations and transitions
-- Immediate feedback for user actions
-- Undo/redo functionality for critical operations
-- Smart defaults and auto-save features
-
-### Visual Design
-- Modern, clean interface with plenty of whitespace
-- Consistent color scheme and typography
-- Clear visual hierarchy with proper contrast ratios
-- Iconography that supports comprehension
-
-## 8. Integration Requirements
-
-### Development Tools
-- VS Code extension with task panel and quick actions
-- Cursor IDE integration with AI task suggestions
-- Terminal CLI for command-line workflow
-- Browser extension for web-based tools
-
-### Third-Party Services
-- GitHub/GitLab integration for issue sync
-- Slack/Discord notifications
-- Calendar integration (Google Calendar, Outlook)
-- Time tracking tools (Toggl, Harvest)
-
-### APIs and Webhooks
-- RESTful API with comprehensive documentation
-- GraphQL endpoint for complex queries
-- Webhook system for external integrations
-- SDK development for major programming languages
-
-## 9. Implementation Phases
-
-### Phase 1: Core MVP (8-10 weeks)
-- Basic task management (CRUD operations)
-- Simple AI task generation
-- Web interface with essential features
-- User authentication and basic permissions
-
-### Phase 2: Enhanced Features (6-8 weeks)
-- Advanced AI features (complexity analysis, subtask generation)
-- Real-time collaboration
-- Mobile-responsive design
-- Integration with one development tool (VS Code)
-
-### Phase 3: Enterprise Features (4-6 weeks)
-- Advanced user management and permissions
-- API and webhook system
-- Performance optimization
-- Comprehensive testing and security audit
-
-### Phase 4: Ecosystem Expansion (4-6 weeks)
-- Additional tool integrations
-- Mobile app development
-- Advanced analytics and reporting
-- Third-party marketplace preparation
-
-## 10. Risk Assessment
-
-### Technical Risks
-- AI model reliability and cost management
-- Real-time synchronization complexity
-- Database performance with large datasets
-- Integration complexity with multiple tools
-
-### Business Risks
-- User adoption in competitive market
-- AI provider dependency
-- Data privacy and security concerns
-- Feature scope creep and timeline delays
-
-### Mitigation Strategies
-- Implement robust error handling and fallback systems
-- Develop comprehensive testing strategy
-- Create detailed documentation and user guides
-- Establish clear project scope and change management process
-
-## 11. Success Criteria
-
-### Development Milestones
-- Alpha version with core features completed
-- Beta version with selected user group feedback
-- Production-ready version with full feature set
-- Post-launch iterations based on user feedback
-
-### Business Metrics
-- User engagement and retention rates
-- Task completion and productivity metrics
-- Customer satisfaction scores (NPS > 50)
-- Revenue targets and subscription growth
-
-## 12. Appendices
-
-### Glossary
-- **PRD**: Product Requirements Document
-- **AI**: Artificial Intelligence
-- **CRUD**: Create, Read, Update, Delete
-- **API**: Application Programming Interface
-- **CI/CD**: Continuous Integration/Continuous Deployment
-
-### References
-- Industry best practices for task management
-- AI integration patterns and examples
-- Security and compliance requirements
-- Performance benchmarking data
-
----
-
-**Document Control:**
-- Version: 1.0
-- Last Updated: December 27, 2024
-- Next Review: January 15, 2025
-- Approved By: Product Owner, Technical Lead`;
-
- // Initialize filename and load content
- useEffect(() => {
- const initializeEditor = async () => {
- // Set initial filename
- if (file?.name) {
- setFileName(file.name.replace(/\.(txt|md)$/, '')); // Remove extension for editing
- } else if (isNewFile) {
- // Generate default filename based on current date
- const now = new Date();
- const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD
- setFileName(`prd-${dateStr}`);
- }
-
- // Load content
- if (isNewFile) {
- setContent(PRD_TEMPLATE);
- setLoading(false);
- return;
- }
-
- // If content is directly provided (for existing PRDs loaded from API)
- if (file.content) {
- setContent(file.content);
- setLoading(false);
- return;
- }
-
- // Fallback to loading from file path (legacy support)
- try {
- setLoading(true);
-
- const response = await api.readFile(file.projectName, file.path);
-
- if (!response.ok) {
- throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
- }
-
- const data = await response.json();
- setContent(data.content || PRD_TEMPLATE);
- } catch (error) {
- console.error('Error loading PRD file:', error);
- setContent(`# Error Loading PRD\n\nError: ${error.message}\n\nFile: ${file?.name || 'New PRD'}\nPath: ${file?.path || 'Not saved yet'}\n\n${PRD_TEMPLATE}`);
- } finally {
- setLoading(false);
- }
- };
-
- initializeEditor();
- }, [file, projectPath, isNewFile]);
-
- // Fetch existing PRDs to check for conflicts
- useEffect(() => {
- const fetchExistingPRDs = async () => {
- if (!project?.name) {
- console.log('No project name available:', project);
- return;
- }
-
- try {
- console.log('Fetching PRDs for project:', project.name);
- const response = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
- if (response.ok) {
- const data = await response.json();
- console.log('Fetched existing PRDs:', data.prds);
- setExistingPRDs(data.prds || []);
- } else {
- console.log('Failed to fetch PRDs:', response.status, response.statusText);
- }
- } catch (error) {
- console.error('Error fetching existing PRDs:', error);
- }
- };
-
- fetchExistingPRDs();
- }, [project?.name]);
-
- const handleSave = async () => {
- if (!content.trim()) {
- alert('Please add content before saving.');
- return;
- }
-
- if (!fileName.trim()) {
- alert('Please provide a filename for the PRD.');
- return;
- }
-
- // Check if file already exists
- const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
- const existingFile = existingPRDs.find(prd => prd.name === fullFileName);
-
- console.log('Save check:', {
- fullFileName,
- existingPRDs,
- existingFile,
- isExisting: file?.isExisting,
- fileObject: file,
- shouldShowModal: existingFile && !file?.isExisting
- });
-
- if (existingFile && !file?.isExisting) {
- console.log('Showing overwrite confirmation modal');
- // Show confirmation modal for overwrite
- setShowOverwriteConfirm(true);
- return;
- }
-
- await performSave();
- };
-
- const performSave = async () => {
- setSaving(true);
- try {
- // Ensure filename has .txt extension
- const fullFileName = fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`;
-
- const response = await authenticatedFetch(`/api/taskmaster/prd/${encodeURIComponent(project?.name)}`, {
- method: 'POST',
- body: JSON.stringify({
- fileName: fullFileName,
- content
- })
- });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.message || `Save failed: ${response.status}`);
- }
-
- // Show success feedback
- setSaveSuccess(true);
- setTimeout(() => setSaveSuccess(false), 2000);
-
- // Update existing PRDs list
- const response2 = await api.get(`/taskmaster/prd/${encodeURIComponent(project.name)}`);
- if (response2.ok) {
- const data = await response2.json();
- setExistingPRDs(data.prds || []);
- }
-
- // Call the onSave callback if provided (for UI updates)
- if (onSave) {
- await onSave();
- }
-
- } catch (error) {
- console.error('Error saving PRD:', error);
- alert(`Error saving PRD: ${error.message}`);
- } finally {
- setSaving(false);
- }
- };
-
- const handleDownload = () => {
- const blob = new Blob([content], { type: 'text/markdown' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- const downloadFileName = fileName ? `${fileName}.txt` : 'prd.txt';
- a.download = downloadFileName;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
-
- const handleGenerateTasks = async () => {
- if (!content.trim()) {
- alert('Please add content to the PRD before generating tasks.');
- return;
- }
-
- // Show AI-first modal instead of simple confirm
- setShowGenerateModal(true);
- };
-
-
- const toggleFullscreen = () => {
- setIsFullscreen(!isFullscreen);
- };
-
- // Handle keyboard shortcuts
- useEffect(() => {
- const handleKeyDown = (e) => {
- if (e.ctrlKey || e.metaKey) {
- if (e.key === 's') {
- e.preventDefault();
- handleSave();
- } else if (e.key === 'Escape') {
- e.preventDefault();
- onClose();
- }
- }
- };
-
- document.addEventListener('keydown', handleKeyDown);
- return () => document.removeEventListener('keydown', handleKeyDown);
- }, [content]);
-
- // Simple markdown to HTML converter for preview
- const renderMarkdown = (markdown) => {
- return markdown
- .replace(/^### (.*$)/gim, '$1 ')
- .replace(/^## (.*$)/gim, '$1 ')
- .replace(/^# (.*$)/gim, '$1 ')
- .replace(/\*\*(.*)\*\*/gim, '$1 ')
- .replace(/\*(.*)\*/gim, '$1 ')
- .replace(/^\- (.*$)/gim, '$1 ')
- .replace(/(.*<\/li>)/gims, '')
- .replace(/\n\n/gim, '
')
- .replace(/^(?!<[h|u|l])(.*$)/gim, '
$1
')
- .replace(/<\/ul>\s*/gim, '');
- };
-
- if (loading) {
- return (
-
- );
- }
-
- return (
-
-
- {/* Header */}
-
-
-
-
-
-
- {/* Mobile: Stack filename and tags vertically for more space */}
-
- {/* Filename input row - full width on mobile */}
-
-
- {
- // Remove invalid filename characters
- const sanitizedValue = e.target.value.replace(/[<>:"/\\|?*]/g, '');
- setFileName(sanitizedValue);
- }}
- className="font-medium text-gray-900 dark:text-white bg-transparent border-none outline-none min-w-0 flex-1 text-base sm:text-sm placeholder-gray-400 dark:placeholder-gray-500"
- placeholder="Enter PRD filename"
- maxLength={100}
- />
- .txt
-
-
document.querySelector('input[placeholder="Enter PRD filename"]')?.focus()}
- className="p-1 text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
- title="Click to edit filename"
- >
-
-
-
-
-
-
- {/* Tags row - moves to second line on mobile for more filename space */}
-
-
- 📋 PRD
-
- {isNewFile && (
-
- ✨ New
-
- )}
-
-
-
- {/* Description - smaller on mobile */}
-
- Product Requirements Document
-
-
-
-
-
-
setPreviewMode(!previewMode)}
- className={cn(
- 'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
- 'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
- previewMode
- ? 'text-purple-600 dark:text-purple-400 bg-purple-50 dark:bg-purple-900'
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
- )}
- title={previewMode ? 'Switch to edit mode' : 'Preview markdown'}
- >
-
-
-
-
setWordWrap(!wordWrap)}
- className={cn(
- 'p-2 md:p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800',
- 'min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center',
- wordWrap
- ? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900'
- : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
- )}
- title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
- >
- ↵
-
-
-
setIsDarkMode(!isDarkMode)}
- className="p-2 md:p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
- title="Toggle theme"
- >
- {isDarkMode ? '☀️' : '🌙'}
-
-
-
-
-
-
-
-
- Generate Tasks
-
-
-
- {saveSuccess ? (
- <>
-
-
-
- Saved!
- >
- ) : (
- <>
-
- {saving ? 'Saving...' : 'Save PRD'}
- >
- )}
-
-
-
- {isFullscreen ? : }
-
-
-
-
-
-
-
-
- {/* Editor/Preview Content */}
-
- {previewMode ? (
-
- ) : (
-
- )}
-
-
- {/* Footer */}
-
-
- Lines: {content.split('\n').length}
- Characters: {content.length}
- Words: {content.split(/\s+/).filter(word => word.length > 0).length}
- Format: Markdown
-
-
-
- Press Ctrl+S to save • Esc to close
-
-
-
-
- {/* Generate Tasks Modal */}
- {showGenerateModal && (
-
-
- {/* Header */}
-
-
-
-
-
-
Generate Tasks from PRD
-
-
setShowGenerateModal(false)}
- className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
- >
-
-
-
-
- {/* Content */}
-
- {/* AI-First Approach */}
-
-
-
-
-
-
-
- 💡 Pro Tip: Ask Claude Code Directly!
-
-
- You can simply ask Claude Code in the chat to parse your PRD and generate tasks.
- The AI assistant will automatically save your PRD and create detailed tasks with implementation details.
-
-
-
-
💬 Example:
-
- "I've just initialized a new project with Claude Task Master. I have a PRD at .taskmaster/docs/{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}. Can you help me parse it and set up the initial tasks?"
-
-
-
-
- This will: Save your PRD, analyze its content, and generate structured tasks with subtasks, dependencies, and implementation details.
-
-
-
-
-
- {/* Learn More Link */}
-
-
- {/* Footer */}
-
- setShowGenerateModal(false)}
- className="w-full px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
- >
- Got it, I'll ask Claude Code directly
-
-
-
-
-
- )}
-
- {/* Overwrite Confirmation Modal */}
- {showOverwriteConfirm && (
-
-
setShowOverwriteConfirm(false)} />
-
-
-
-
-
- File Already Exists
-
-
-
-
- A PRD file named "{fileName.endsWith('.txt') || fileName.endsWith('.md') ? fileName : `${fileName}.txt`}" already exists.
- Do you want to overwrite it with the current content?
-
-
-
- setShowOverwriteConfirm(false)}
- className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
- >
- Cancel
-
- {
- setShowOverwriteConfirm(false);
- await performSave();
- }}
- className="px-4 py-2 text-sm text-white bg-yellow-600 hover:bg-yellow-700 rounded-md flex items-center space-x-2 transition-colors"
- >
-
- Overwrite
-
-
-
-
-
- )}
-
- );
-};
-
-export default PRDEditor;
\ No newline at end of file
diff --git a/src/components/PRDEditor.tsx b/src/components/PRDEditor.tsx
new file mode 100644
index 00000000..80a59f24
--- /dev/null
+++ b/src/components/PRDEditor.tsx
@@ -0,0 +1 @@
+export { default } from './prd-editor';
diff --git a/src/components/main-content/view/subcomponents/TaskMasterPanel.tsx b/src/components/main-content/view/subcomponents/TaskMasterPanel.tsx
index 91810cfe..27554f29 100644
--- a/src/components/main-content/view/subcomponents/TaskMasterPanel.tsx
+++ b/src/components/main-content/view/subcomponents/TaskMasterPanel.tsx
@@ -9,7 +9,6 @@ import type { PrdFile, TaskMasterPanelProps, TaskMasterTask, TaskSelection } fro
const AnyTaskList = TaskList as any;
const AnyTaskDetail = TaskDetail as any;
-const AnyPRDEditor = PRDEditor as any;
type TaskMasterContextValue = {
tasks?: TaskMasterTask[];
@@ -178,7 +177,7 @@ export default function TaskMasterPanel({ isVisible }: TaskMasterPanelProps) {
)}
{showPRDEditor && (
-
diff --git a/src/components/prd-editor/PRDEditor.tsx b/src/components/prd-editor/PRDEditor.tsx
new file mode 100644
index 00000000..3ad89d63
--- /dev/null
+++ b/src/components/prd-editor/PRDEditor.tsx
@@ -0,0 +1,138 @@
+import { useCallback, useMemo, useState } from 'react';
+import type { Project } from '../../types/app';
+import { usePrdDocument } from './hooks/usePrdDocument';
+import { usePrdKeyboardShortcuts } from './hooks/usePrdKeyboardShortcuts';
+import { usePrdRegistry } from './hooks/usePrdRegistry';
+import { usePrdSave } from './hooks/usePrdSave';
+import type { PrdFile } from './types';
+import { ensurePrdExtension } from './utils/fileName';
+import OverwriteConfirmModal from './view/OverwriteConfirmModal';
+import PrdEditorLoadingState from './view/PrdEditorLoadingState';
+import PrdEditorWorkspace from './view/PrdEditorWorkspace';
+
+type PRDEditorProps = {
+ file?: PrdFile | null;
+ onClose: () => void;
+ projectPath?: string;
+ project?: Project | null;
+ initialContent?: string;
+ isNewFile?: boolean;
+ onSave?: () => Promise
| void;
+};
+
+export default function PRDEditor({
+ file,
+ onClose,
+ projectPath,
+ project,
+ initialContent = '',
+ isNewFile = false,
+ onSave,
+}: PRDEditorProps) {
+ const [showOverwriteConfirm, setShowOverwriteConfirm] = useState(false);
+ const [overwriteFileName, setOverwriteFileName] = useState('');
+
+ const { content, setContent, fileName, setFileName, loading, loadError } = usePrdDocument({
+ file,
+ isNewFile,
+ initialContent,
+ projectPath,
+ });
+
+ const { existingPrds, refreshExistingPrds } = usePrdRegistry({
+ projectName: project?.name,
+ });
+
+ const isExistingFile = useMemo(() => !isNewFile || Boolean(file?.isExisting), [file?.isExisting, isNewFile]);
+
+ const { savePrd, saving, saveSuccess } = usePrdSave({
+ projectName: project?.name,
+ existingPrds,
+ isExistingFile,
+ onAfterSave: async () => {
+ await refreshExistingPrds();
+ await onSave?.();
+ },
+ });
+
+ const handleDownload = useCallback(() => {
+ const blob = new Blob([content], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+ const anchor = document.createElement('a');
+ const downloadedFileName = ensurePrdExtension(fileName || 'prd');
+
+ anchor.href = url;
+ anchor.download = downloadedFileName;
+ document.body.appendChild(anchor);
+ anchor.click();
+ document.body.removeChild(anchor);
+ URL.revokeObjectURL(url);
+ }, [content, fileName]);
+
+ const handleSave = useCallback(
+ async (allowOverwrite = false) => {
+ const result = await savePrd({
+ content,
+ fileName,
+ allowOverwrite,
+ });
+
+ if (result.status === 'needs-overwrite') {
+ setOverwriteFileName(result.fileName);
+ setShowOverwriteConfirm(true);
+ return;
+ }
+
+ if (result.status === 'failed') {
+ alert(result.message);
+ }
+ },
+ [content, fileName, savePrd],
+ );
+
+ const confirmOverwrite = useCallback(async () => {
+ setShowOverwriteConfirm(false);
+ await handleSave(true);
+ }, [handleSave]);
+
+ usePrdKeyboardShortcuts({
+ onSave: () => {
+ void handleSave();
+ },
+ onClose,
+ });
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+ {
+ void handleSave();
+ }}
+ onDownload={handleDownload}
+ onClose={onClose}
+ loadError={loadError}
+ />
+
+ setShowOverwriteConfirm(false)}
+ onConfirm={() => {
+ void confirmOverwrite();
+ }}
+ />
+ >
+ );
+}
diff --git a/src/components/prd-editor/constants.ts b/src/components/prd-editor/constants.ts
new file mode 100644
index 00000000..527d03f8
--- /dev/null
+++ b/src/components/prd-editor/constants.ts
@@ -0,0 +1,67 @@
+export const PRD_TEMPLATE = `# Product Requirements Document
+
+## 1. Overview
+- Product Name:
+- Owner:
+- Last Updated:
+- Version:
+
+Describe what problem this product solves and who it serves.
+
+## 2. Objectives
+- Primary objective:
+- Secondary objective:
+- Out-of-scope:
+
+## 3. User Stories
+- As a , I want , so that .
+- As a , I want , so that .
+
+## 4. Functional Requirements
+### Core Requirements
+- Requirement 1
+- Requirement 2
+
+### Edge Cases
+- Edge case 1
+- Edge case 2
+
+## 5. Non-Functional Requirements
+- Performance:
+- Security:
+- Reliability:
+- Accessibility:
+
+## 6. Success Metrics
+- Metric 1:
+- Metric 2:
+
+## 7. Technical Notes
+- Architecture constraints:
+- Dependencies:
+- Integration points:
+
+## 8. Release Plan
+### Milestone 1
+- Scope:
+- Exit criteria:
+
+### Milestone 2
+- Scope:
+- Exit criteria:
+
+## 9. Risks and Mitigations
+- Risk:
+ - Impact:
+ - Mitigation:
+
+## 10. Open Questions
+- Question 1
+- Question 2
+`;
+
+export const PRD_DOCS_URL =
+ 'https://github.com/eyaltoledano/claude-task-master/blob/main/docs/examples.md';
+
+export const INVALID_FILE_NAME_CHARACTERS = /[<>:"/\\|?*]/g;
+export const PRD_EXTENSION_PATTERN = /\.(txt|md)$/i;
diff --git a/src/components/prd-editor/hooks/usePrdDocument.ts b/src/components/prd-editor/hooks/usePrdDocument.ts
new file mode 100644
index 00000000..3728caf4
--- /dev/null
+++ b/src/components/prd-editor/hooks/usePrdDocument.ts
@@ -0,0 +1,132 @@
+import { useCallback, useEffect, useState } from 'react';
+import { api } from '../../../utils/api';
+import { PRD_TEMPLATE } from '../constants';
+import type { PrdFile } from '../types';
+import { createDefaultPrdName, sanitizeFileName, stripPrdExtension } from '../utils/fileName';
+
+type UsePrdDocumentArgs = {
+ file?: PrdFile | null;
+ isNewFile: boolean;
+ initialContent: string;
+ projectPath?: string;
+};
+
+type UsePrdDocumentResult = {
+ content: string;
+ setContent: (nextContent: string) => void;
+ fileName: string;
+ setFileName: (nextFileName: string) => void;
+ loading: boolean;
+ loadError: string | null;
+};
+
+export function usePrdDocument({
+ file,
+ isNewFile,
+ initialContent,
+ projectPath,
+}: UsePrdDocumentArgs): UsePrdDocumentResult {
+ const [content, setContent] = useState(initialContent || '');
+ const [fileName, setFileNameState] = useState('');
+ const [loading, setLoading] = useState(!isNewFile);
+ const [loadError, setLoadError] = useState(null);
+
+ const setFileName = useCallback((nextFileName: string) => {
+ setFileNameState(sanitizeFileName(nextFileName));
+ }, []);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const initialize = async () => {
+ const defaultName = file?.name
+ ? stripPrdExtension(file.name)
+ : createDefaultPrdName(new Date());
+
+ if (isMounted) {
+ setFileNameState(defaultName);
+ }
+
+ // Loading precedence:
+ // 1) new file -> initial content or template
+ // 2) provided content -> use it directly
+ // 3) legacy file path -> fetch from API
+ if (isNewFile) {
+ if (!isMounted) {
+ return;
+ }
+
+ setContent(initialContent || PRD_TEMPLATE);
+ setLoadError(null);
+ setLoading(false);
+ return;
+ }
+
+ if (file?.content) {
+ if (!isMounted) {
+ return;
+ }
+
+ setContent(file.content);
+ setLoadError(null);
+ setLoading(false);
+ return;
+ }
+
+ if (!file?.projectName || !file?.path) {
+ if (!isMounted) {
+ return;
+ }
+
+ setContent(initialContent || PRD_TEMPLATE);
+ setLoadError(null);
+ setLoading(false);
+ return;
+ }
+
+ try {
+ setLoading(true);
+
+ const response = await api.readFile(file.projectName, file.path);
+ if (!response.ok) {
+ throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
+ }
+
+ const data = (await response.json()) as { content?: string };
+ if (!isMounted) {
+ return;
+ }
+
+ setContent(data.content || PRD_TEMPLATE);
+ setLoadError(null);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Unknown error';
+ if (!isMounted) {
+ return;
+ }
+
+ setContent(initialContent || PRD_TEMPLATE);
+ setLoadError(`Unable to load file content: ${message}`);
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ void initialize();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [file, initialContent, isNewFile, projectPath]);
+
+ return {
+ content,
+ setContent,
+ fileName,
+ setFileName,
+ loading,
+ loadError,
+ };
+}
diff --git a/src/components/prd-editor/hooks/usePrdKeyboardShortcuts.ts b/src/components/prd-editor/hooks/usePrdKeyboardShortcuts.ts
new file mode 100644
index 00000000..8834f791
--- /dev/null
+++ b/src/components/prd-editor/hooks/usePrdKeyboardShortcuts.ts
@@ -0,0 +1,34 @@
+import { useEffect } from 'react';
+
+type UsePrdKeyboardShortcutsArgs = {
+ onSave: () => void;
+ onClose: () => void;
+};
+
+export function usePrdKeyboardShortcuts({
+ onSave,
+ onClose,
+}: UsePrdKeyboardShortcutsArgs): void {
+ useEffect(() => {
+ // Keep shortcuts global so the editor behaves consistently in fullscreen and modal mode.
+ const handleKeyDown = (event: KeyboardEvent) => {
+ const loweredKey = event.key.toLowerCase();
+
+ if ((event.ctrlKey || event.metaKey) && loweredKey === 's') {
+ event.preventDefault();
+ onSave();
+ return;
+ }
+
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ onClose();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [onClose, onSave]);
+}
diff --git a/src/components/prd-editor/hooks/usePrdRegistry.ts b/src/components/prd-editor/hooks/usePrdRegistry.ts
new file mode 100644
index 00000000..f7a40856
--- /dev/null
+++ b/src/components/prd-editor/hooks/usePrdRegistry.ts
@@ -0,0 +1,50 @@
+import { useCallback, useEffect, useState } from 'react';
+import { api } from '../../../utils/api';
+import type { ExistingPrdFile, PrdListResponse } from '../types';
+
+type UsePrdRegistryArgs = {
+ projectName?: string;
+};
+
+type UsePrdRegistryResult = {
+ existingPrds: ExistingPrdFile[];
+ refreshExistingPrds: () => Promise;
+};
+
+function getPrdFiles(data: PrdListResponse): ExistingPrdFile[] {
+ return data.prdFiles || data.prds || [];
+}
+
+export function usePrdRegistry({ projectName }: UsePrdRegistryArgs): UsePrdRegistryResult {
+ const [existingPrds, setExistingPrds] = useState([]);
+
+ const refreshExistingPrds = useCallback(async () => {
+ if (!projectName) {
+ setExistingPrds([]);
+ return;
+ }
+
+ try {
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(projectName)}`);
+ if (!response.ok) {
+ setExistingPrds([]);
+ return;
+ }
+
+ const data = (await response.json()) as PrdListResponse;
+ setExistingPrds(getPrdFiles(data));
+ } catch (error) {
+ console.error('Failed to fetch existing PRDs:', error);
+ setExistingPrds([]);
+ }
+ }, [projectName]);
+
+ useEffect(() => {
+ void refreshExistingPrds();
+ }, [refreshExistingPrds]);
+
+ return {
+ existingPrds,
+ refreshExistingPrds,
+ };
+}
diff --git a/src/components/prd-editor/hooks/usePrdSave.ts b/src/components/prd-editor/hooks/usePrdSave.ts
new file mode 100644
index 00000000..1d802ad5
--- /dev/null
+++ b/src/components/prd-editor/hooks/usePrdSave.ts
@@ -0,0 +1,111 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { authenticatedFetch } from '../../../utils/api';
+import type { ExistingPrdFile, SavePrdInput, SavePrdResult } from '../types';
+import { ensurePrdExtension } from '../utils/fileName';
+
+type UsePrdSaveArgs = {
+ projectName?: string;
+ existingPrds: ExistingPrdFile[];
+ isExistingFile: boolean;
+ onAfterSave?: () => Promise;
+};
+
+type UsePrdSaveResult = {
+ savePrd: (input: SavePrdInput) => Promise;
+ saving: boolean;
+ saveSuccess: boolean;
+};
+
+export function usePrdSave({
+ projectName,
+ existingPrds,
+ isExistingFile,
+ onAfterSave,
+}: UsePrdSaveArgs): UsePrdSaveResult {
+ const [saving, setSaving] = useState(false);
+ const [saveSuccess, setSaveSuccess] = useState(false);
+ const saveSuccessTimeoutRef = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (saveSuccessTimeoutRef.current) {
+ clearTimeout(saveSuccessTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const savePrd = useCallback(
+ async ({ content, fileName, allowOverwrite = false }: SavePrdInput): Promise => {
+ if (!content.trim()) {
+ return { status: 'failed', message: 'Please add content before saving.' };
+ }
+
+ if (!fileName.trim()) {
+ return { status: 'failed', message: 'Please provide a filename for the PRD.' };
+ }
+
+ if (!projectName) {
+ return { status: 'failed', message: 'No project selected. Please reopen the editor.' };
+ }
+
+ const finalFileName = ensurePrdExtension(fileName.trim());
+ const hasConflict = existingPrds.some((prd) => prd.name === finalFileName);
+
+ // Overwrite confirmation is only required when creating a brand-new PRD.
+ if (hasConflict && !allowOverwrite && !isExistingFile) {
+ return { status: 'needs-overwrite', fileName: finalFileName };
+ }
+
+ setSaving(true);
+
+ try {
+ const response = await authenticatedFetch(`/api/taskmaster/prd/${encodeURIComponent(projectName)}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ fileName: finalFileName,
+ content,
+ }),
+ });
+
+ if (!response.ok) {
+ const fallbackMessage = `Save failed: ${response.status}`;
+
+ try {
+ const errorData = (await response.json()) as { message?: string };
+ return { status: 'failed', message: errorData.message || fallbackMessage };
+ } catch {
+ return { status: 'failed', message: fallbackMessage };
+ }
+ }
+
+ if (saveSuccessTimeoutRef.current) {
+ clearTimeout(saveSuccessTimeoutRef.current);
+ }
+
+ setSaveSuccess(true);
+ saveSuccessTimeoutRef.current = setTimeout(() => {
+ setSaveSuccess(false);
+ saveSuccessTimeoutRef.current = null;
+ }, 2000);
+
+ if (onAfterSave) {
+ await onAfterSave();
+ }
+
+ return { status: 'saved', fileName: finalFileName };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Unknown error';
+ return { status: 'failed', message: `Error saving PRD: ${message}` };
+ } finally {
+ setSaving(false);
+ }
+ },
+ [existingPrds, isExistingFile, onAfterSave, projectName],
+ );
+
+ return {
+ savePrd,
+ saving,
+ saveSuccess,
+ };
+}
diff --git a/src/components/prd-editor/index.ts b/src/components/prd-editor/index.ts
new file mode 100644
index 00000000..1732efcf
--- /dev/null
+++ b/src/components/prd-editor/index.ts
@@ -0,0 +1 @@
+export { default } from './PRDEditor';
diff --git a/src/components/prd-editor/types.ts b/src/components/prd-editor/types.ts
new file mode 100644
index 00000000..c9ddfd85
--- /dev/null
+++ b/src/components/prd-editor/types.ts
@@ -0,0 +1,30 @@
+export type PrdFile = {
+ name?: string;
+ path?: string;
+ projectName?: string;
+ content?: string;
+ isExisting?: boolean;
+};
+
+export type ExistingPrdFile = {
+ name: string;
+ content?: string;
+ isExisting?: boolean;
+ [key: string]: unknown;
+};
+
+export type PrdListResponse = {
+ prdFiles?: ExistingPrdFile[];
+ prds?: ExistingPrdFile[];
+};
+
+export type SavePrdInput = {
+ content: string;
+ fileName: string;
+ allowOverwrite?: boolean;
+};
+
+export type SavePrdResult =
+ | { status: 'saved'; fileName: string }
+ | { status: 'needs-overwrite'; fileName: string }
+ | { status: 'failed'; message: string };
diff --git a/src/components/prd-editor/utils/fileName.ts b/src/components/prd-editor/utils/fileName.ts
new file mode 100644
index 00000000..47c0eb4a
--- /dev/null
+++ b/src/components/prd-editor/utils/fileName.ts
@@ -0,0 +1,18 @@
+import { INVALID_FILE_NAME_CHARACTERS, PRD_EXTENSION_PATTERN } from '../constants';
+
+export function sanitizeFileName(value: string): string {
+ return value.replace(INVALID_FILE_NAME_CHARACTERS, '');
+}
+
+export function stripPrdExtension(value: string): string {
+ return value.replace(PRD_EXTENSION_PATTERN, '');
+}
+
+export function ensurePrdExtension(value: string): string {
+ return PRD_EXTENSION_PATTERN.test(value) ? value : `${value}.txt`;
+}
+
+export function createDefaultPrdName(date: Date): string {
+ const isoDate = date.toISOString().split('T')[0];
+ return `prd-${isoDate}`;
+}
diff --git a/src/components/prd-editor/view/GenerateTasksModal.tsx b/src/components/prd-editor/view/GenerateTasksModal.tsx
new file mode 100644
index 00000000..ca1ad280
--- /dev/null
+++ b/src/components/prd-editor/view/GenerateTasksModal.tsx
@@ -0,0 +1,77 @@
+import { Sparkles, X } from 'lucide-react';
+import { PRD_DOCS_URL } from '../constants';
+
+type GenerateTasksModalProps = {
+ isOpen: boolean;
+ fileName: string;
+ onClose: () => void;
+};
+
+export default function GenerateTasksModal({
+ isOpen,
+ fileName,
+ onClose,
+}: GenerateTasksModalProps) {
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ Generate Tasks from PRD
+
+
+
+
+
+
+
+
+
+
+ Ask Claude Code directly
+
+
+ Save this PRD, then ask Claude Code in chat to parse the file and create your initial tasks.
+
+
+
+
Example prompt
+
+ I have a PRD at .taskmaster/docs/{fileName}. Parse it and create the initial tasks.
+
+
+
+
+
+
+
+ Got it
+
+
+
+
+ );
+}
diff --git a/src/components/prd-editor/view/OverwriteConfirmModal.tsx b/src/components/prd-editor/view/OverwriteConfirmModal.tsx
new file mode 100644
index 00000000..ec5a94ad
--- /dev/null
+++ b/src/components/prd-editor/view/OverwriteConfirmModal.tsx
@@ -0,0 +1,60 @@
+import { AlertTriangle, Save } from 'lucide-react';
+
+type OverwriteConfirmModalProps = {
+ isOpen: boolean;
+ fileName: string;
+ saving: boolean;
+ onCancel: () => void;
+ onConfirm: () => void;
+};
+
+export default function OverwriteConfirmModal({
+ isOpen,
+ fileName,
+ saving,
+ onCancel,
+ onConfirm,
+}: OverwriteConfirmModalProps) {
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
File Already Exists
+
+
+
+ A PRD named "{fileName}" already exists. Do you want to overwrite it?
+
+
+
+
+ Cancel
+
+
+
+ {saving ? 'Saving...' : 'Overwrite'}
+
+
+
+
+
+ );
+}
diff --git a/src/components/prd-editor/view/PrdEditorBody.tsx b/src/components/prd-editor/view/PrdEditorBody.tsx
new file mode 100644
index 00000000..62988431
--- /dev/null
+++ b/src/components/prd-editor/view/PrdEditorBody.tsx
@@ -0,0 +1,61 @@
+import { useMemo } from 'react';
+import { markdown } from '@codemirror/lang-markdown';
+import { oneDark } from '@codemirror/theme-one-dark';
+import { EditorView } from '@codemirror/view';
+import CodeMirror from '@uiw/react-codemirror';
+import MarkdownPreview from '../../code-editor/view/subcomponents/markdown/MarkdownPreview';
+
+type PrdEditorBodyProps = {
+ content: string;
+ onContentChange: (nextContent: string) => void;
+ previewMode: boolean;
+ isDarkMode: boolean;
+ wordWrap: boolean;
+};
+
+export default function PrdEditorBody({
+ content,
+ onContentChange,
+ previewMode,
+ isDarkMode,
+ wordWrap,
+}: PrdEditorBodyProps) {
+ const extensions = useMemo(
+ () => [markdown(), ...(wordWrap ? [EditorView.lineWrapping] : [])],
+ [wordWrap],
+ );
+
+ if (previewMode) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/prd-editor/view/PrdEditorFooter.tsx b/src/components/prd-editor/view/PrdEditorFooter.tsx
new file mode 100644
index 00000000..352febb8
--- /dev/null
+++ b/src/components/prd-editor/view/PrdEditorFooter.tsx
@@ -0,0 +1,36 @@
+import { useMemo } from 'react';
+
+type PrdEditorFooterProps = {
+ content: string;
+};
+
+type ContentStats = {
+ lines: number;
+ characters: number;
+ words: number;
+};
+
+function getContentStats(content: string): ContentStats {
+ return {
+ lines: content.split('\n').length,
+ characters: content.length,
+ words: content.split(/\s+/).filter(Boolean).length,
+ };
+}
+
+export default function PrdEditorFooter({ content }: PrdEditorFooterProps) {
+ const stats = useMemo(() => getContentStats(content), [content]);
+
+ return (
+
+
+ Lines: {stats.lines}
+ Characters: {stats.characters}
+ Words: {stats.words}
+ Format: Markdown
+
+
+
Press Ctrl+S to save and Esc to close
+
+ );
+}
diff --git a/src/components/prd-editor/view/PrdEditorHeader.tsx b/src/components/prd-editor/view/PrdEditorHeader.tsx
new file mode 100644
index 00000000..0b673bed
--- /dev/null
+++ b/src/components/prd-editor/view/PrdEditorHeader.tsx
@@ -0,0 +1,228 @@
+import { useRef } from 'react';
+import type { ReactNode } from 'react';
+import {
+ Download,
+ Eye,
+ FileText,
+ Maximize2,
+ Minimize2,
+ Moon,
+ Save,
+ Sparkles,
+ Sun,
+ X,
+} from 'lucide-react';
+import { cn } from '../../../lib/utils';
+
+type PrdEditorHeaderProps = {
+ fileName: string;
+ onFileNameChange: (nextFileName: string) => void;
+ isNewFile: boolean;
+ previewMode: boolean;
+ onTogglePreview: () => void;
+ wordWrap: boolean;
+ onToggleWordWrap: () => void;
+ isDarkMode: boolean;
+ onToggleTheme: () => void;
+ onDownload: () => void;
+ onOpenGenerateTasks: () => void;
+ canGenerateTasks: boolean;
+ onSave: () => void;
+ saving: boolean;
+ saveSuccess: boolean;
+ isFullscreen: boolean;
+ onToggleFullscreen: () => void;
+ onClose: () => void;
+};
+
+type HeaderIconButtonProps = {
+ title: string;
+ onClick: () => void;
+ icon: ReactNode;
+ active?: boolean;
+};
+
+function HeaderIconButton({ title, onClick, icon, active = false }: HeaderIconButtonProps) {
+ return (
+
+ {icon}
+
+ );
+}
+
+export default function PrdEditorHeader({
+ fileName,
+ onFileNameChange,
+ isNewFile,
+ previewMode,
+ onTogglePreview,
+ wordWrap,
+ onToggleWordWrap,
+ isDarkMode,
+ onToggleTheme,
+ onDownload,
+ onOpenGenerateTasks,
+ canGenerateTasks,
+ onSave,
+ saving,
+ saveSuccess,
+ isFullscreen,
+ onToggleFullscreen,
+ onClose,
+}: PrdEditorHeaderProps) {
+ const fileNameInputRef = useRef(null);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ onFileNameChange(event.target.value)}
+ className="font-medium text-gray-900 dark:text-white bg-transparent border-none outline-none min-w-0 flex-1 text-base sm:text-sm placeholder-gray-400 dark:placeholder-gray-500"
+ placeholder="Enter PRD filename"
+ maxLength={100}
+ />
+
+ .txt
+
+
+
+
fileNameInputRef.current?.focus()}
+ className="p-1 text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
+ title="Focus filename input"
+ >
+
+
+
+
+
+
+
+
+ PRD
+
+ {isNewFile && (
+
+ New
+
+ )}
+
+
+
+
+ Product Requirements Document
+
+
+
+
+
+
}
+ active={previewMode}
+ />
+
+
WRAP}
+ active={wordWrap}
+ />
+
+
+ ) : (
+
+ )
+ }
+ />
+
+ }
+ />
+
+
+
+ Generate Tasks
+
+
+
+ {saveSuccess ? (
+ <>
+
+
+
+ Saved!
+ >
+ ) : (
+ <>
+
+ {saving ? 'Saving...' : 'Save PRD'}
+ >
+ )}
+
+
+
+ {isFullscreen ? : }
+
+
+ }
+ />
+
+
+ );
+}
diff --git a/src/components/prd-editor/view/PrdEditorLoadingState.tsx b/src/components/prd-editor/view/PrdEditorLoadingState.tsx
new file mode 100644
index 00000000..38cee6a9
--- /dev/null
+++ b/src/components/prd-editor/view/PrdEditorLoadingState.tsx
@@ -0,0 +1,12 @@
+export default function PrdEditorLoadingState() {
+ return (
+
+ );
+}
diff --git a/src/components/prd-editor/view/PrdEditorWorkspace.tsx b/src/components/prd-editor/view/PrdEditorWorkspace.tsx
new file mode 100644
index 00000000..f3cb1d2d
--- /dev/null
+++ b/src/components/prd-editor/view/PrdEditorWorkspace.tsx
@@ -0,0 +1,114 @@
+import { useState } from 'react';
+import { cn } from '../../../lib/utils';
+import { ensurePrdExtension } from '../utils/fileName';
+import GenerateTasksModal from './GenerateTasksModal';
+import PrdEditorBody from './PrdEditorBody';
+import PrdEditorFooter from './PrdEditorFooter';
+import PrdEditorHeader from './PrdEditorHeader';
+
+type PrdEditorWorkspaceProps = {
+ content: string;
+ onContentChange: (nextContent: string) => void;
+ fileName: string;
+ onFileNameChange: (nextFileName: string) => void;
+ isNewFile: boolean;
+ saving: boolean;
+ saveSuccess: boolean;
+ onSave: () => void;
+ onDownload: () => void;
+ onClose: () => void;
+ loadError: string | null;
+};
+
+export default function PrdEditorWorkspace({
+ content,
+ onContentChange,
+ fileName,
+ onFileNameChange,
+ isNewFile,
+ saving,
+ saveSuccess,
+ onSave,
+ onDownload,
+ onClose,
+ loadError,
+}: PrdEditorWorkspaceProps) {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+ const [isDarkMode, setIsDarkMode] = useState(true);
+ const [previewMode, setPreviewMode] = useState(false);
+ const [wordWrap, setWordWrap] = useState(true);
+ const [showGenerateModal, setShowGenerateModal] = useState(false);
+
+ const handleOpenGenerateTasks = () => {
+ if (!content.trim()) {
+ alert('Please add content to the PRD before generating tasks.');
+ return;
+ }
+
+ setShowGenerateModal(true);
+ };
+
+ return (
+
+
+ {loadError && (
+
+ {loadError}
+
+ )}
+
+
setPreviewMode((current) => !current)}
+ wordWrap={wordWrap}
+ onToggleWordWrap={() => setWordWrap((current) => !current)}
+ isDarkMode={isDarkMode}
+ onToggleTheme={() => setIsDarkMode((current) => !current)}
+ onDownload={onDownload}
+ onOpenGenerateTasks={handleOpenGenerateTasks}
+ canGenerateTasks={Boolean(content.trim())}
+ onSave={onSave}
+ saving={saving}
+ saveSuccess={saveSuccess}
+ isFullscreen={isFullscreen}
+ onToggleFullscreen={() => setIsFullscreen((current) => !current)}
+ onClose={onClose}
+ />
+
+
+
+
+
+
+
setShowGenerateModal(false)}
+ />
+
+ );
+}