diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx
deleted file mode 100644
index fb7002ea..00000000
--- a/src/components/MainContent.jsx
+++ /dev/null
@@ -1,713 +0,0 @@
-/*
- * MainContent.jsx - Main Content Area with Session Protection Props Passthrough
- *
- * SESSION PROTECTION PASSTHROUGH:
- * ===============================
- *
- * This component serves as a passthrough layer for Session Protection functions:
- * - Receives session management functions from App.jsx
- * - Passes them down to ChatInterface.jsx
- *
- * No session protection logic is implemented here - it's purely a props bridge.
- */
-
-import React, { useState, useEffect, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-import ChatInterface from './ChatInterface';
-import FileTree from './FileTree';
-import CodeEditor from './CodeEditor';
-import StandaloneShell from './StandaloneShell';
-import GitPanel from './GitPanel';
-import ErrorBoundary from './ErrorBoundary';
-import ClaudeLogo from './ClaudeLogo';
-import CursorLogo from './CursorLogo';
-import TaskList from './TaskList';
-import TaskDetail from './TaskDetail';
-import PRDEditor from './PRDEditor';
-import Tooltip from './Tooltip';
-import { useTaskMaster } from '../contexts/TaskMasterContext';
-import { useTasksSettings } from '../contexts/TasksSettingsContext';
-import { api } from '../utils/api';
-
-import { useUiPreferences } from '../hooks/useUiPreferences';
-
-function MainContent({
- selectedProject,
- selectedSession,
- activeTab,
- setActiveTab,
- ws,
- sendMessage,
- latestMessage,
- isMobile,
- onMenuClick,
- isLoading,
- onInputFocusChange,
- // Session Protection Props: Functions passed down from App.jsx to manage active session state
- // These functions control when project updates are paused during active conversations
- onSessionActive, // Mark session as active when user sends message
- onSessionInactive, // Mark session as inactive when conversation completes/aborts
- onSessionProcessing, // Mark session as processing (thinking/working)
- onSessionNotProcessing, // Mark session as not processing (finished thinking)
- processingSessions, // Set of session IDs currently processing
- onReplaceTemporarySession, // Replace temporary session ID with real session ID from WebSocket
- onNavigateToSession, // Navigate to a specific session (for Claude CLI session duplication workaround)
- onShowSettings, // Show tools settings panel
- externalMessageUpdate // Trigger for external CLI updates to current session
-}) {
- const { t } = useTranslation();
- const [editingFile, setEditingFile] = useState(null);
- const [selectedTask, setSelectedTask] = useState(null);
- const [showTaskDetail, setShowTaskDetail] = useState(false);
- const [editorWidth, setEditorWidth] = useState(600);
- const [isResizing, setIsResizing] = useState(false);
- const [editorExpanded, setEditorExpanded] = useState(false);
- const resizeRef = useRef(null);
- const suppressNextMenuClickRef = useRef(false);
-
- const { preferences } = useUiPreferences();
- const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
-
- // PRD Editor state
- const [showPRDEditor, setShowPRDEditor] = useState(false);
- const [selectedPRD, setSelectedPRD] = useState(null);
- const [existingPRDs, setExistingPRDs] = useState([]);
- const [prdNotification, setPRDNotification] = useState(null);
-
- // TaskMaster context
- const { tasks, currentProject, refreshTasks, setCurrentProject } = useTaskMaster();
- const { tasksEnabled, isTaskMasterInstalled, isTaskMasterReady } = useTasksSettings();
-
- // Only show tasks tab if TaskMaster is installed and enabled
- const shouldShowTasksTab = tasksEnabled && isTaskMasterInstalled;
-
- // Sync selectedProject with TaskMaster context
- useEffect(() => {
- if (selectedProject && selectedProject !== currentProject) {
- setCurrentProject(selectedProject);
- }
- }, [selectedProject, currentProject, setCurrentProject]);
-
- // Switch away from tasks tab when tasks are disabled or TaskMaster is not installed
- useEffect(() => {
- if (!shouldShowTasksTab && activeTab === 'tasks') {
- setActiveTab('chat');
- }
- }, [shouldShowTasksTab, activeTab, setActiveTab]);
-
- // Load existing PRDs when current project changes
- useEffect(() => {
- const loadExistingPRDs = async () => {
- if (!currentProject?.name) {
- setExistingPRDs([]);
- return;
- }
-
- try {
- const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
- if (response.ok) {
- const data = await response.json();
- setExistingPRDs(data.prdFiles || []);
- } else {
- setExistingPRDs([]);
- }
- } catch (error) {
- console.error('Failed to load existing PRDs:', error);
- setExistingPRDs([]);
- }
- };
-
- loadExistingPRDs();
- }, [currentProject?.name]);
-
- const handleFileOpen = (filePath, diffInfo = null) => {
- // Create a file object that CodeEditor expects
- const file = {
- name: filePath.split('/').pop(),
- path: filePath,
- projectName: selectedProject?.name,
- diffInfo: diffInfo // Pass along diff information if available
- };
- setEditingFile(file);
- };
-
- const handleCloseEditor = () => {
- setEditingFile(null);
- setEditorExpanded(false);
- };
-
- const handleToggleEditorExpand = () => {
- setEditorExpanded(!editorExpanded);
- };
-
- const handleTaskClick = (task) => {
- // If task is just an ID (from dependency click), find the full task object
- if (typeof task === 'object' && task.id && !task.title) {
- const fullTask = tasks?.find(t => t.id === task.id);
- if (fullTask) {
- setSelectedTask(fullTask);
- setShowTaskDetail(true);
- }
- } else {
- setSelectedTask(task);
- setShowTaskDetail(true);
- }
- };
-
- const handleTaskDetailClose = () => {
- setShowTaskDetail(false);
- setSelectedTask(null);
- };
-
- const handleTaskStatusChange = (taskId, newStatus) => {
- // This would integrate with TaskMaster API to update task status
- console.log('Update task status:', taskId, newStatus);
- refreshTasks?.();
- };
-
- const openMobileMenu = (event) => {
- if (event) {
- event.preventDefault();
- event.stopPropagation();
- }
-
- onMenuClick();
- };
-
- const handleMobileMenuTouchEnd = (event) => {
- suppressNextMenuClickRef.current = true;
- openMobileMenu(event);
-
- window.setTimeout(() => {
- suppressNextMenuClickRef.current = false;
- }, 350);
- };
-
- const handleMobileMenuClick = (event) => {
- if (suppressNextMenuClickRef.current) {
- event.preventDefault();
- event.stopPropagation();
- return;
- }
-
- openMobileMenu(event);
- };
-
- // Handle resize functionality
- const handleMouseDown = (e) => {
- if (isMobile) return; // Disable resize on mobile
- setIsResizing(true);
- e.preventDefault();
- };
-
- useEffect(() => {
- const handleMouseMove = (e) => {
- if (!isResizing) return;
-
- const container = resizeRef.current?.parentElement;
- if (!container) return;
-
- const containerRect = container.getBoundingClientRect();
- const newWidth = containerRect.right - e.clientX;
-
- // Min width: 300px, Max width: 80% of container
- const minWidth = 300;
- const maxWidth = containerRect.width * 0.8;
-
- if (newWidth >= minWidth && newWidth <= maxWidth) {
- setEditorWidth(newWidth);
- }
- };
-
- const handleMouseUp = () => {
- setIsResizing(false);
- };
-
- if (isResizing) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = 'col-resize';
- document.body.style.userSelect = 'none';
- }
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- document.body.style.cursor = '';
- document.body.style.userSelect = '';
- };
- }, [isResizing]);
-
- if (isLoading) {
- return (
-
- {/* Header with menu button for mobile */}
- {isMobile && (
-
- )}
-
-
-
-
{t('mainContent.loading')}
-
{t('mainContent.settingUpWorkspace')}
-
-
-
- );
- }
-
- if (!selectedProject) {
- return (
-
- {/* Header with menu button for mobile */}
- {isMobile && (
-
- )}
-
-
-
-
{t('mainContent.chooseProject')}
-
- {t('mainContent.selectProjectDescription')}
-
-
-
- 💡 {t('mainContent.tip')}: {isMobile ? t('mainContent.createProjectMobile') : t('mainContent.createProjectDesktop')}
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Header with tabs */}
-
-
-
- {isMobile && (
-
- )}
-
- {activeTab === 'chat' && selectedSession && (
-
- {selectedSession.__provider === 'cursor' ? (
-
- ) : (
-
- )}
-
- )}
-
- {activeTab === 'chat' && selectedSession ? (
-
-
- {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')}
-
-
- {selectedProject.displayName}
-
-
- ) : activeTab === 'chat' && !selectedSession ? (
-
-
- {t('mainContent.newSession')}
-
-
- {selectedProject.displayName}
-
-
- ) : (
-
-
- {activeTab === 'files' ? t('mainContent.projectFiles') :
- activeTab === 'git' ? t('tabs.git') :
- (activeTab === 'tasks' && shouldShowTasksTab) ? 'TaskMaster' :
- 'Project'}
-
-
- {selectedProject.displayName}
-
-
- )}
-
-
-
-
- {/* Modern Tab Navigation - Right Side */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {shouldShowTasksTab && (
-
-
-
- )}
- {/*
*/}
-
-
-
-
-
- {/* Content Area with Right Sidebar */}
-
- {/* Main Content */}
-
-
-
- setActiveTab('tasks') : null}
- />
-
-
- {activeTab === 'files' && (
-
-
-
- )}
- {activeTab === 'shell' && (
-
-
-
- )}
- {activeTab === 'git' && (
-
-
-
- )}
- {shouldShowTasksTab && (
-
-
- {
- setSelectedPRD(prd);
- setShowPRDEditor(true);
- }}
- existingPRDs={existingPRDs}
- onRefreshPRDs={(showNotification = false) => {
- // Reload existing PRDs
- if (currentProject?.name) {
- api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`)
- .then(response => response.ok ? response.json() : Promise.reject())
- .then(data => {
- setExistingPRDs(data.prdFiles || []);
- if (showNotification) {
- setPRDNotification('PRD saved successfully!');
- setTimeout(() => setPRDNotification(null), 3000);
- }
- })
- .catch(error => console.error('Failed to refresh PRDs:', error));
- }
- }}
- />
-
-
- )}
-
- {/* {
- sendMessage({
- type: 'server:start',
- projectPath: selectedProject?.fullPath,
- script: script
- });
- }}
- onStopServer={() => {
- sendMessage({
- type: 'server:stop',
- projectPath: selectedProject?.fullPath
- });
- }}
- onScriptSelect={setCurrentScript}
- currentScript={currentScript}
- isMobile={isMobile}
- serverLogs={serverLogs}
- onClearLogs={() => setServerLogs([])}
- /> */}
-
-
-
- {/* Code Editor Right Sidebar - Desktop only, Mobile uses modal */}
- {editingFile && !isMobile && (
- <>
- {/* Resize Handle - Hidden when expanded */}
- {!editorExpanded && (
-
- {/* Visual indicator on hover */}
-
-
- )}
-
- {/* Editor Sidebar */}
-
-
-
- >
- )}
-
-
- {/* Code Editor Modal for Mobile */}
- {editingFile && isMobile && (
-
- )}
-
- {/* Task Detail Modal */}
- {shouldShowTasksTab && showTaskDetail && selectedTask && (
-
- )}
- {/* PRD Editor Modal */}
- {showPRDEditor && (
-
{
- setShowPRDEditor(false);
- setSelectedPRD(null);
- }}
- isNewFile={!selectedPRD?.isExisting}
- file={{
- name: selectedPRD?.name || 'prd.txt',
- content: selectedPRD?.content || ''
- }}
- onSave={async () => {
- setShowPRDEditor(false);
- setSelectedPRD(null);
-
- // Reload existing PRDs with notification
- try {
- const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
- if (response.ok) {
- const data = await response.json();
- setExistingPRDs(data.prdFiles || []);
- setPRDNotification('PRD saved successfully!');
- setTimeout(() => setPRDNotification(null), 3000);
- }
- } catch (error) {
- console.error('Failed to refresh PRDs:', error);
- }
-
- refreshTasks?.();
- }}
- />
- )}
- {/* PRD Notification */}
- {prdNotification && (
-
-
-
-
{prdNotification}
-
-
- )}
-
- );
-}
-
-export default React.memo(MainContent);
diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx
new file mode 100644
index 00000000..4ea9b187
--- /dev/null
+++ b/src/components/MainContent.tsx
@@ -0,0 +1,182 @@
+import React, { useEffect } from 'react';
+
+import ChatInterface from './ChatInterface';
+import FileTree from './FileTree';
+import StandaloneShell from './StandaloneShell';
+import GitPanel from './GitPanel';
+import ErrorBoundary from './ErrorBoundary';
+
+import MainContentHeader from './main-content/MainContentHeader';
+import MainContentStateView from './main-content/MainContentStateView';
+import EditorSidebar from './main-content/EditorSidebar';
+import TaskMasterPanel from './main-content/TaskMasterPanel';
+import type { MainContentProps } from './main-content/types';
+
+import { useTaskMaster } from '../contexts/TaskMasterContext';
+import { useTasksSettings } from '../contexts/TasksSettingsContext';
+import { useUiPreferences } from '../hooks/useUiPreferences';
+import { useEditorSidebar } from '../hooks/main-content/useEditorSidebar';
+import type { Project } from '../types/app';
+
+const AnyChatInterface = ChatInterface as any;
+const AnyStandaloneShell = StandaloneShell as any;
+const AnyGitPanel = GitPanel as any;
+
+type TaskMasterContextValue = {
+ currentProject?: Project | null;
+ setCurrentProject?: ((project: Project) => void) | null;
+};
+
+type TasksSettingsContextValue = {
+ tasksEnabled: boolean;
+ isTaskMasterInstalled: boolean | null;
+ isTaskMasterReady: boolean | null;
+};
+
+function MainContent({
+ selectedProject,
+ selectedSession,
+ activeTab,
+ setActiveTab,
+ ws,
+ sendMessage,
+ latestMessage,
+ isMobile,
+ onMenuClick,
+ isLoading,
+ onInputFocusChange,
+ onSessionActive,
+ onSessionInactive,
+ onSessionProcessing,
+ onSessionNotProcessing,
+ processingSessions,
+ onReplaceTemporarySession,
+ onNavigateToSession,
+ onShowSettings,
+ externalMessageUpdate,
+}: MainContentProps) {
+ const { preferences } = useUiPreferences();
+ const { autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter } = preferences;
+
+ const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue;
+ const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue;
+
+ const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled);
+
+ const {
+ editingFile,
+ editorWidth,
+ editorExpanded,
+ resizeHandleRef,
+ handleFileOpen,
+ handleCloseEditor,
+ handleToggleEditorExpand,
+ handleResizeStart,
+ } = useEditorSidebar({
+ selectedProject,
+ isMobile,
+ });
+
+ useEffect(() => {
+ if (selectedProject && selectedProject !== currentProject) {
+ setCurrentProject?.(selectedProject);
+ }
+ }, [selectedProject, currentProject, setCurrentProject]);
+
+ useEffect(() => {
+ if (!shouldShowTasksTab && activeTab === 'tasks') {
+ setActiveTab('chat');
+ }
+ }, [shouldShowTasksTab, activeTab, setActiveTab]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!selectedProject) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ setActiveTab('tasks') : null}
+ />
+
+
+
+ {activeTab === 'files' && (
+
+
+
+ )}
+
+ {activeTab === 'shell' && (
+
+ )}
+
+ {activeTab === 'git' && (
+
+ )}
+
+ {shouldShowTasksTab &&
}
+
+
+
+
+
+
+
+ );
+}
+
+export default React.memo(MainContent);
diff --git a/src/components/main-content/EditorSidebar.tsx b/src/components/main-content/EditorSidebar.tsx
new file mode 100644
index 00000000..78fe2c41
--- /dev/null
+++ b/src/components/main-content/EditorSidebar.tsx
@@ -0,0 +1,60 @@
+import CodeEditor from '../CodeEditor';
+import type { EditorSidebarProps } from './types';
+
+const AnyCodeEditor = CodeEditor as any;
+
+export default function EditorSidebar({
+ editingFile,
+ isMobile,
+ editorExpanded,
+ editorWidth,
+ resizeHandleRef,
+ onResizeStart,
+ onCloseEditor,
+ onToggleEditorExpand,
+ projectPath,
+}: EditorSidebarProps) {
+ if (!editingFile) {
+ return null;
+ }
+
+ if (isMobile) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+ {!editorExpanded && (
+
+ )}
+
+
+ >
+ );
+}
diff --git a/src/components/main-content/MainContentHeader.tsx b/src/components/main-content/MainContentHeader.tsx
new file mode 100644
index 00000000..81e37391
--- /dev/null
+++ b/src/components/main-content/MainContentHeader.tsx
@@ -0,0 +1,38 @@
+import MobileMenuButton from './MobileMenuButton';
+import MainContentTabSwitcher from './MainContentTabSwitcher';
+import MainContentTitle from './MainContentTitle';
+import type { MainContentHeaderProps } from './types';
+
+export default function MainContentHeader({
+ activeTab,
+ setActiveTab,
+ selectedProject,
+ selectedSession,
+ shouldShowTasksTab,
+ isMobile,
+ onMenuClick,
+}: MainContentHeaderProps) {
+ return (
+
+
+
+ {isMobile && }
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/main-content/MainContentStateView.tsx b/src/components/main-content/MainContentStateView.tsx
new file mode 100644
index 00000000..f9b4e143
--- /dev/null
+++ b/src/components/main-content/MainContentStateView.tsx
@@ -0,0 +1,55 @@
+import { useTranslation } from 'react-i18next';
+import MobileMenuButton from './MobileMenuButton';
+import type { MainContentStateViewProps } from './types';
+
+export default function MainContentStateView({ mode, isMobile, onMenuClick }: MainContentStateViewProps) {
+ const { t } = useTranslation();
+
+ const isLoading = mode === 'loading';
+
+ return (
+
+ {isMobile && (
+
+
+
+ )}
+
+ {isLoading ? (
+
+
+
+
{t('mainContent.loading')}
+
{t('mainContent.settingUpWorkspace')}
+
+
+ ) : (
+
+
+
+
{t('mainContent.chooseProject')}
+
{t('mainContent.selectProjectDescription')}
+
+
+ {t('mainContent.tip')}: {isMobile ? t('mainContent.createProjectMobile') : t('mainContent.createProjectDesktop')}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/main-content/MainContentTabSwitcher.tsx b/src/components/main-content/MainContentTabSwitcher.tsx
new file mode 100644
index 00000000..56251654
--- /dev/null
+++ b/src/components/main-content/MainContentTabSwitcher.tsx
@@ -0,0 +1,84 @@
+import Tooltip from '../Tooltip';
+import type { AppTab } from '../../types/app';
+import type { Dispatch, SetStateAction } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type MainContentTabSwitcherProps = {
+ activeTab: AppTab;
+ setActiveTab: Dispatch>;
+ shouldShowTasksTab: boolean;
+};
+
+type TabDefinition = {
+ id: AppTab;
+ labelKey: string;
+ iconPath: string;
+};
+
+const BASE_TABS: TabDefinition[] = [
+ {
+ id: 'chat',
+ labelKey: 'tabs.chat',
+ iconPath:
+ 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z',
+ },
+ {
+ id: 'shell',
+ labelKey: 'tabs.shell',
+ iconPath: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z',
+ },
+ {
+ id: 'files',
+ labelKey: 'tabs.files',
+ iconPath: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-5l-2-2H5a2 2 0 00-2 2z',
+ },
+ {
+ id: 'git',
+ labelKey: 'tabs.git',
+ iconPath: 'M13 10V3L4 14h7v7l9-11h-7z',
+ },
+];
+
+const TASKS_TAB: TabDefinition = {
+ id: 'tasks',
+ labelKey: 'tabs.tasks',
+ iconPath:
+ 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4',
+};
+
+function getButtonClasses(tabId: AppTab, activeTab: AppTab) {
+ const base = 'relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium rounded-md transition-all duration-200';
+
+ if (tabId === activeTab) {
+ return `${base} bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm`;
+ }
+
+ return `${base} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-700`;
+}
+
+export default function MainContentTabSwitcher({
+ activeTab,
+ setActiveTab,
+ shouldShowTasksTab,
+}: MainContentTabSwitcherProps) {
+ const { t } = useTranslation();
+
+ const tabs = shouldShowTasksTab ? [...BASE_TABS, TASKS_TAB] : BASE_TABS;
+
+ return (
+
+ {tabs.map((tab) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/components/main-content/MainContentTitle.tsx b/src/components/main-content/MainContentTitle.tsx
new file mode 100644
index 00000000..7d2773b6
--- /dev/null
+++ b/src/components/main-content/MainContentTitle.tsx
@@ -0,0 +1,84 @@
+import { useTranslation } from 'react-i18next';
+import ClaudeLogo from '../ClaudeLogo';
+import CursorLogo from '../CursorLogo';
+import type { AppTab, Project, ProjectSession } from '../../types/app';
+
+type MainContentTitleProps = {
+ activeTab: AppTab;
+ selectedProject: Project;
+ selectedSession: ProjectSession | null;
+ shouldShowTasksTab: boolean;
+};
+
+function getTabTitle(activeTab: AppTab, shouldShowTasksTab: boolean, t: (key: string) => string) {
+ if (activeTab === 'files') {
+ return t('mainContent.projectFiles');
+ }
+
+ if (activeTab === 'git') {
+ return t('tabs.git');
+ }
+
+ if (activeTab === 'tasks' && shouldShowTasksTab) {
+ return 'TaskMaster';
+ }
+
+ return 'Project';
+}
+
+function getSessionTitle(session: ProjectSession): string {
+ if (session.__provider === 'cursor') {
+ return (session.name as string) || 'Untitled Session';
+ }
+
+ return (session.summary as string) || 'New Session';
+}
+
+export default function MainContentTitle({
+ activeTab,
+ selectedProject,
+ selectedSession,
+ shouldShowTasksTab,
+}: MainContentTitleProps) {
+ const { t } = useTranslation();
+
+ const showSessionIcon = activeTab === 'chat' && Boolean(selectedSession);
+ const showChatNewSession = activeTab === 'chat' && !selectedSession;
+
+ return (
+
+ {showSessionIcon && (
+
+ {selectedSession?.__provider === 'cursor' ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {activeTab === 'chat' && selectedSession ? (
+
+
+ {getSessionTitle(selectedSession)}
+
+
{selectedProject.displayName}
+
+ ) : showChatNewSession ? (
+
+
{t('mainContent.newSession')}
+
{selectedProject.displayName}
+
+ ) : (
+
+
+ {getTabTitle(activeTab, shouldShowTasksTab, t)}
+
+
{selectedProject.displayName}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/main-content/MobileMenuButton.tsx b/src/components/main-content/MobileMenuButton.tsx
new file mode 100644
index 00000000..405558e2
--- /dev/null
+++ b/src/components/main-content/MobileMenuButton.tsx
@@ -0,0 +1,23 @@
+import type { MobileMenuButtonProps } from './types';
+import { useMobileMenuHandlers } from '../../hooks/main-content/useMobileMenuHandlers';
+
+export default function MobileMenuButton({ onMenuClick, compact = false }: MobileMenuButtonProps) {
+ const { handleMobileMenuClick, handleMobileMenuTouchEnd } = useMobileMenuHandlers(onMenuClick);
+
+ const buttonClasses = compact
+ ? '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-700 pwa-menu-button'
+ : '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-700 touch-manipulation active:scale-95 pwa-menu-button flex-shrink-0';
+
+ return (
+
+ );
+}
diff --git a/src/components/main-content/TaskMasterPanel.tsx b/src/components/main-content/TaskMasterPanel.tsx
new file mode 100644
index 00000000..cc7df384
--- /dev/null
+++ b/src/components/main-content/TaskMasterPanel.tsx
@@ -0,0 +1,206 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import TaskList from '../TaskList';
+import TaskDetail from '../TaskDetail';
+import PRDEditor from '../PRDEditor';
+import { useTaskMaster } from '../../contexts/TaskMasterContext';
+import { api } from '../../utils/api';
+import type { Project } from '../../types/app';
+import type { PrdFile, TaskMasterPanelProps, TaskMasterTask, TaskSelection } from './types';
+
+const AnyTaskList = TaskList as any;
+const AnyTaskDetail = TaskDetail as any;
+const AnyPRDEditor = PRDEditor as any;
+
+type TaskMasterContextValue = {
+ tasks?: TaskMasterTask[];
+ currentProject?: Project | null;
+ refreshTasks?: (() => void) | null;
+};
+
+type PrdListResponse = {
+ prdFiles?: PrdFile[];
+ prds?: PrdFile[];
+};
+
+const PRD_SAVED_MESSAGE = 'PRD saved successfully!';
+
+function getPrdFiles(data: PrdListResponse): PrdFile[] {
+ return data.prdFiles || data.prds || [];
+}
+
+export default function TaskMasterPanel({ isVisible }: TaskMasterPanelProps) {
+ const { tasks = [], currentProject, refreshTasks } = useTaskMaster() as TaskMasterContextValue;
+
+ const [selectedTask, setSelectedTask] = useState(null);
+ const [showTaskDetail, setShowTaskDetail] = useState(false);
+
+ const [showPRDEditor, setShowPRDEditor] = useState(false);
+ const [selectedPRD, setSelectedPRD] = useState(null);
+ const [existingPRDs, setExistingPRDs] = useState([]);
+ const [prdNotification, setPRDNotification] = useState(null);
+
+ const prdNotificationTimeoutRef = useRef | null>(null);
+
+ const showPrdNotification = useCallback((message: string) => {
+ if (prdNotificationTimeoutRef.current) {
+ clearTimeout(prdNotificationTimeoutRef.current);
+ }
+
+ setPRDNotification(message);
+ prdNotificationTimeoutRef.current = setTimeout(() => {
+ setPRDNotification(null);
+ prdNotificationTimeoutRef.current = null;
+ }, 3000);
+ }, []);
+
+ const loadExistingPrds = useCallback(async () => {
+ if (!currentProject?.name) {
+ setExistingPRDs([]);
+ return;
+ }
+
+ try {
+ const response = await api.get(`/taskmaster/prd/${encodeURIComponent(currentProject.name)}`);
+ if (!response.ok) {
+ setExistingPRDs([]);
+ return;
+ }
+
+ const data = (await response.json()) as PrdListResponse;
+ setExistingPRDs(getPrdFiles(data));
+ } catch (error) {
+ console.error('Failed to load existing PRDs:', error);
+ setExistingPRDs([]);
+ }
+ }, [currentProject?.name]);
+
+ const refreshPrds = useCallback(
+ async (showNotification = false) => {
+ await loadExistingPrds();
+
+ if (showNotification) {
+ showPrdNotification(PRD_SAVED_MESSAGE);
+ }
+ },
+ [loadExistingPrds, showPrdNotification],
+ );
+
+ useEffect(() => {
+ void loadExistingPrds();
+ }, [loadExistingPrds]);
+
+ useEffect(() => {
+ return () => {
+ if (prdNotificationTimeoutRef.current) {
+ clearTimeout(prdNotificationTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ const handleTaskClick = useCallback(
+ (task: TaskSelection) => {
+ if (!task || typeof task !== 'object' || !('id' in task)) {
+ return;
+ }
+
+ if (!('title' in task) || !task.title) {
+ const fullTask = tasks.find((candidate) => String(candidate.id) === String(task.id));
+ if (fullTask) {
+ setSelectedTask(fullTask);
+ setShowTaskDetail(true);
+ }
+ return;
+ }
+
+ setSelectedTask(task as TaskMasterTask);
+ setShowTaskDetail(true);
+ },
+ [tasks],
+ );
+
+ const handleTaskDetailClose = useCallback(() => {
+ setShowTaskDetail(false);
+ setSelectedTask(null);
+ }, []);
+
+ const handleTaskStatusChange = useCallback(
+ (taskId: string | number, newStatus: string) => {
+ console.log('Update task status:', taskId, newStatus);
+ refreshTasks?.();
+ },
+ [refreshTasks],
+ );
+
+ const handleOpenPrdEditor = useCallback((prd: PrdFile | null = null) => {
+ setSelectedPRD(prd);
+ setShowPRDEditor(true);
+ }, []);
+
+ const handleClosePrdEditor = useCallback(() => {
+ setShowPRDEditor(false);
+ setSelectedPRD(null);
+ }, []);
+
+ const handlePrdSave = useCallback(async () => {
+ handleClosePrdEditor();
+ await refreshPrds(true);
+ refreshTasks?.();
+ }, [handleClosePrdEditor, refreshPrds, refreshTasks]);
+
+ return (
+ <>
+
+
+
{
+ void refreshPrds(showNotification);
+ }}
+ />
+
+
+
+ {showTaskDetail && selectedTask && (
+
+ )}
+
+ {showPRDEditor && (
+
+ )}
+
+ {prdNotification && (
+
+
+
+
{prdNotification}
+
+
+ )}
+ >
+ );
+}
diff --git a/src/components/main-content/types.ts b/src/components/main-content/types.ts
new file mode 100644
index 00000000..2ebcfc3c
--- /dev/null
+++ b/src/components/main-content/types.ts
@@ -0,0 +1,107 @@
+import type { Dispatch, MouseEvent, RefObject, SetStateAction } from 'react';
+import type { AppTab, Project, ProjectSession } from '../../types/app';
+
+export type SessionLifecycleHandler = (sessionId?: string | null) => void;
+
+export interface DiffInfo {
+ old_string?: string;
+ new_string?: string;
+ [key: string]: unknown;
+}
+
+export interface EditingFile {
+ name: string;
+ path: string;
+ projectName?: string;
+ diffInfo?: DiffInfo | null;
+ [key: string]: unknown;
+}
+
+export interface TaskMasterTask {
+ id: string | number;
+ title?: string;
+ description?: string;
+ status?: string;
+ priority?: string;
+ details?: string;
+ testStrategy?: string;
+ parentId?: string | number;
+ dependencies?: Array;
+ subtasks?: TaskMasterTask[];
+ [key: string]: unknown;
+}
+
+export interface TaskReference {
+ id: string | number;
+ title?: string;
+ [key: string]: unknown;
+}
+
+export type TaskSelection = TaskMasterTask | TaskReference;
+
+export interface PrdFile {
+ name: string;
+ content?: string;
+ isExisting?: boolean;
+ [key: string]: unknown;
+}
+
+export interface MainContentProps {
+ selectedProject: Project | null;
+ selectedSession: ProjectSession | null;
+ activeTab: AppTab;
+ setActiveTab: Dispatch>;
+ ws: WebSocket | null;
+ sendMessage: (message: unknown) => void;
+ latestMessage: unknown;
+ isMobile: boolean;
+ onMenuClick: () => void;
+ isLoading: boolean;
+ onInputFocusChange: (focused: boolean) => void;
+ onSessionActive: SessionLifecycleHandler;
+ onSessionInactive: SessionLifecycleHandler;
+ onSessionProcessing: SessionLifecycleHandler;
+ onSessionNotProcessing: SessionLifecycleHandler;
+ processingSessions: Set;
+ onReplaceTemporarySession: SessionLifecycleHandler;
+ onNavigateToSession: (targetSessionId: string) => void;
+ onShowSettings: () => void;
+ externalMessageUpdate: number;
+}
+
+export interface MainContentHeaderProps {
+ activeTab: AppTab;
+ setActiveTab: Dispatch>;
+ selectedProject: Project;
+ selectedSession: ProjectSession | null;
+ shouldShowTasksTab: boolean;
+ isMobile: boolean;
+ onMenuClick: () => void;
+}
+
+export interface MainContentStateViewProps {
+ mode: 'loading' | 'empty';
+ isMobile: boolean;
+ onMenuClick: () => void;
+}
+
+export interface MobileMenuButtonProps {
+ onMenuClick: () => void;
+ compact?: boolean;
+}
+
+export interface EditorSidebarProps {
+ editingFile: EditingFile | null;
+ isMobile: boolean;
+ editorExpanded: boolean;
+ editorWidth: number;
+ resizeHandleRef: RefObject;
+ onResizeStart: (event: MouseEvent) => void;
+ onCloseEditor: () => void;
+ onToggleEditorExpand: () => void;
+ projectPath?: string;
+}
+
+export interface TaskMasterPanelProps {
+ isVisible: boolean;
+}
diff --git a/src/hooks/main-content/useEditorSidebar.ts b/src/hooks/main-content/useEditorSidebar.ts
new file mode 100644
index 00000000..9ca57fed
--- /dev/null
+++ b/src/hooks/main-content/useEditorSidebar.ts
@@ -0,0 +1,110 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import type { MouseEvent as ReactMouseEvent } from 'react';
+import type { Project } from '../../types/app';
+import type { DiffInfo, EditingFile } from '../../components/main-content/types';
+
+type UseEditorSidebarOptions = {
+ selectedProject: Project | null;
+ isMobile: boolean;
+ initialWidth?: number;
+};
+
+export function useEditorSidebar({
+ selectedProject,
+ isMobile,
+ initialWidth = 600,
+}: UseEditorSidebarOptions) {
+ const [editingFile, setEditingFile] = useState(null);
+ const [editorWidth, setEditorWidth] = useState(initialWidth);
+ const [editorExpanded, setEditorExpanded] = useState(false);
+ const [isResizing, setIsResizing] = useState(false);
+ const resizeHandleRef = useRef(null);
+
+ const handleFileOpen = useCallback(
+ (filePath: string, diffInfo: DiffInfo | null = null) => {
+ const normalizedPath = filePath.replace(/\\/g, '/');
+ const fileName = normalizedPath.split('/').pop() || filePath;
+
+ setEditingFile({
+ name: fileName,
+ path: filePath,
+ projectName: selectedProject?.name,
+ diffInfo,
+ });
+ },
+ [selectedProject?.name],
+ );
+
+ const handleCloseEditor = useCallback(() => {
+ setEditingFile(null);
+ setEditorExpanded(false);
+ }, []);
+
+ const handleToggleEditorExpand = useCallback(() => {
+ setEditorExpanded((prev) => !prev);
+ }, []);
+
+ const handleResizeStart = useCallback(
+ (event: ReactMouseEvent) => {
+ if (isMobile) {
+ return;
+ }
+
+ setIsResizing(true);
+ event.preventDefault();
+ },
+ [isMobile],
+ );
+
+ useEffect(() => {
+ const handleMouseMove = (event: globalThis.MouseEvent) => {
+ if (!isResizing) {
+ return;
+ }
+
+ const container = resizeHandleRef.current?.parentElement;
+ if (!container) {
+ return;
+ }
+
+ const containerRect = container.getBoundingClientRect();
+ const newWidth = containerRect.right - event.clientX;
+
+ const minWidth = 300;
+ const maxWidth = containerRect.width * 0.8;
+
+ if (newWidth >= minWidth && newWidth <= maxWidth) {
+ setEditorWidth(newWidth);
+ }
+ };
+
+ const handleMouseUp = () => {
+ setIsResizing(false);
+ };
+
+ if (isResizing) {
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = 'col-resize';
+ document.body.style.userSelect = 'none';
+ }
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ };
+ }, [isResizing]);
+
+ return {
+ editingFile,
+ editorWidth,
+ editorExpanded,
+ resizeHandleRef,
+ handleFileOpen,
+ handleCloseEditor,
+ handleToggleEditorExpand,
+ handleResizeStart,
+ };
+}
diff --git a/src/hooks/main-content/useMobileMenuHandlers.ts b/src/hooks/main-content/useMobileMenuHandlers.ts
new file mode 100644
index 00000000..d9472b56
--- /dev/null
+++ b/src/hooks/main-content/useMobileMenuHandlers.ts
@@ -0,0 +1,50 @@
+import { useCallback, useRef } from 'react';
+import type { MouseEvent, TouchEvent } from 'react';
+
+type MenuEvent = MouseEvent | TouchEvent;
+
+export function useMobileMenuHandlers(onMenuClick: () => void) {
+ const suppressNextMenuClickRef = useRef(false);
+
+ const openMobileMenu = useCallback(
+ (event?: MenuEvent) => {
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ onMenuClick();
+ },
+ [onMenuClick],
+ );
+
+ const handleMobileMenuTouchEnd = useCallback(
+ (event: TouchEvent) => {
+ suppressNextMenuClickRef.current = true;
+ openMobileMenu(event);
+
+ window.setTimeout(() => {
+ suppressNextMenuClickRef.current = false;
+ }, 350);
+ },
+ [openMobileMenu],
+ );
+
+ const handleMobileMenuClick = useCallback(
+ (event: MouseEvent) => {
+ if (suppressNextMenuClickRef.current) {
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
+
+ openMobileMenu(event);
+ },
+ [openMobileMenu],
+ );
+
+ return {
+ handleMobileMenuClick,
+ handleMobileMenuTouchEnd,
+ };
+}