diff --git a/src/App.tsx b/src/App.tsx index ea38d614..b4b2f727 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,8 @@ import { SystemUIProvider } from '@/components/refactored/shared/contexts/system import { RootLayout } from '@/components/refactored/shared/layout/RootLayout'; import StandaloneShellRouterAdapter from '@/components/standalone-shell/view/StandaloneShellRouterAdapter'; import FileTreeRouterAdapter from '@/components/file-tree/view/FileTreeRouterAdapter.js'; +import GitPanelRouterAdapter from '@/components/git-panel/view/GitPanelRouterAdapter.js'; +import { TaskMasterPanel } from '@/components/task-master/index.js'; const isValidRouteTab = (value: string | undefined): boolean => { if (!value) { @@ -120,6 +122,8 @@ const router = createBrowserRouter( { index: true, element: }, { path: 'shell', element: }, { path: 'files', element: }, + { path: 'git', element: }, + { path: 'tasks', element: }, { path: ':tab', element: }, ], }, diff --git a/src/components/code-editor/types/types.ts b/src/components/code-editor/types/types.ts index d2737683..b94c15c9 100644 --- a/src/components/code-editor/types/types.ts +++ b/src/components/code-editor/types/types.ts @@ -1,3 +1,5 @@ +export type { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js'; + export type CodeEditorSettingsState = { isDarkMode: boolean; wordWrap: boolean; diff --git a/src/components/code-editor/view/EditorSidebar.tsx b/src/components/code-editor/view/EditorSidebar.tsx index f170a2a3..a5d40da9 100644 --- a/src/components/code-editor/view/EditorSidebar.tsx +++ b/src/components/code-editor/view/EditorSidebar.tsx @@ -2,10 +2,10 @@ import { useState, useEffect, useRef } from 'react'; import type { MouseEvent, MutableRefObject } from 'react'; import CodeEditor from './CodeEditor'; import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js'; +import { useDeviceSettings } from '@/hooks/useDeviceSettings.js'; type EditorSidebarProps = { editingFile: CodeEditorFile | null; - isMobile: boolean; editorExpanded: boolean; editorWidth: number; hasManualWidth: boolean; @@ -24,7 +24,6 @@ const MIN_EDITOR_WIDTH = 280; export default function EditorSidebar({ editingFile, - isMobile, editorExpanded, editorWidth, hasManualWidth, @@ -39,6 +38,8 @@ export default function EditorSidebar({ const containerRef = useRef(null); const [effectiveWidth, setEffectiveWidth] = useState(editorWidth); + const { isMobile } = useDeviceSettings({ trackPWA: false }); + // Adjust editor width when container size changes to ensure buttons are always visible useEffect(() => { if (!editingFile || isMobile || poppedOut) return; diff --git a/src/components/code-editor/view/EditorSidebarRouterAdapter.tsx b/src/components/code-editor/view/EditorSidebarRouterAdapter.tsx new file mode 100644 index 00000000..3a40d930 --- /dev/null +++ b/src/components/code-editor/view/EditorSidebarRouterAdapter.tsx @@ -0,0 +1,114 @@ +/** + * This is for backward compatibility with the old setup that point to the file tree route. + * It fetches the project and session data based on the URL parameters and passes them to the FileTree component. + * If no valid parameters are found, it defaults to an empty project. + * + * TODO: This adapter can be removed once all tabs use the updated projects and sessions format. + */ + +import { useLocation, useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { + getProjectsInLegacyFormat, + getSessionInLegacyFormat, +} from "@/components/refactored/sidebar/data/legacy-response-format-api.js"; +import { Project } from "@/types/app.js"; +import { useSystemUI } from "@/components/refactored/shared/contexts/system-ui-context/useSystemUI"; +import EditorSidebar from "@/components/code-editor/view/EditorSidebar.js"; + +export default function EditorSidebarRouterAdapter() { + const { sessionId, workspaceId } = useParams<{ + sessionId?: string; + workspaceId?: string; + }>(); + const { pathname } = useLocation(); + + const { + codeEditorSidebar: { + editingFile, + editorWidth, + editorExpanded, + hasManualWidth, + resizeHandleRef, + handleCloseEditor, + handleToggleEditorExpand, + handleResizeStart, + }, + } = useSystemUI(); + + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + const fetchProjectAndSession = async () => { + setLoading(true); + + try { + if (workspaceId) { + const fetchedProject = await getProjectsInLegacyFormat(workspaceId); + + if (!cancelled) { + setProject(fetchedProject); + } + + return; + } + + if (sessionId) { + const result = await getSessionInLegacyFormat(sessionId); + + if (!cancelled) { + if (result) { + setProject(result.project); + } + } + + return; + } + + if (!cancelled) { + setProject(null); + } + } catch (error) { + console.error("Failed to fetch project/session:", error); + + if (!cancelled) { + setProject(null); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + fetchProjectAndSession(); + + return () => { + cancelled = true; + }; + }, [sessionId, workspaceId]); + + const fillSpace = pathname.replace(/\/+$/, "").endsWith("/files"); + + if (loading) { + return null; + } + + return ( + + ); +} diff --git a/src/components/file-tree/view/FileTree.tsx b/src/components/file-tree/view/FileTree.tsx index a920a4eb..4907300d 100644 --- a/src/components/file-tree/view/FileTree.tsx +++ b/src/components/file-tree/view/FileTree.tsx @@ -47,7 +47,6 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps) const { files, loading, refreshFiles } = useFileTreeData(selectedProject); - console.log("Files are: ", files) const { viewMode, changeViewMode } = useFileTreeViewMode(); const { expandedDirs, toggleDirectory, expandDirectories, collapseAll } = useExpandedDirectories(); const { searchQuery, setSearchQuery, filteredFiles } = useFileTreeSearch({ diff --git a/src/components/file-tree/view/FileTreeRouterAdapter.tsx b/src/components/file-tree/view/FileTreeRouterAdapter.tsx index 0c73422e..da368ef9 100644 --- a/src/components/file-tree/view/FileTreeRouterAdapter.tsx +++ b/src/components/file-tree/view/FileTreeRouterAdapter.tsx @@ -7,14 +7,14 @@ */ import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { getProjectsInLegacyFormat, getSessionInLegacyFormat, } from "@/components/refactored/sidebar/data/legacy-response-format-api.js"; import { Project } from "@/types/app.js"; import FileTree from "@/components/file-tree/view/FileTree.js"; -import { useEditorSidebar } from "@/hooks/code-editor-sidebar/useEditorSidebar.js"; +import { useSystemUI } from "@/components/refactored/shared/contexts/system-ui-context/useSystemUI"; export default function FileTreeRouterAdapter() { const { sessionId, workspaceId } = useParams<{ @@ -22,7 +22,9 @@ export default function FileTreeRouterAdapter() { workspaceId?: string; }>(); - const { handleFileOpen } = useEditorSidebar({}); + const { + codeEditorSidebar: { handleFileOpen }, + } = useSystemUI(); const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); @@ -79,15 +81,18 @@ export default function FileTreeRouterAdapter() { }; }, [sessionId, workspaceId]); - console.log("FileTreeRouterAdapter project:", project); + const handleProjectFileOpen = useCallback( + (filePath: string) => { + handleFileOpen(filePath, null, project?.name); + }, + [handleFileOpen, project?.name], + ); if (loading) { return
Loading...
; } - console.log("FileTreeRouterAdapter project:", project); - return ( - + ); -} \ No newline at end of file +} diff --git a/src/components/git-panel/types/types.ts b/src/components/git-panel/types/types.ts index 7abf9820..68fc9394 100644 --- a/src/components/git-panel/types/types.ts +++ b/src/components/git-panel/types/types.ts @@ -12,12 +12,6 @@ export type FileDiffInfo = { export type FileOpenHandler = (filePath: string, diffInfo?: FileDiffInfo) => void; -export type GitPanelProps = { - selectedProject: Project | null; - isMobile?: boolean; - onFileOpen?: FileOpenHandler; -}; - export type GitStatusResponse = { branch?: string; hasCommits?: boolean; diff --git a/src/components/git-panel/view/GitPanel.tsx b/src/components/git-panel/view/GitPanel.tsx index fc6438bd..ab623848 100644 --- a/src/components/git-panel/view/GitPanel.tsx +++ b/src/components/git-panel/view/GitPanel.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; import { useGitPanelController } from '../hooks/useGitPanelController'; import { useRevertLocalCommit } from '../hooks/useRevertLocalCommit'; -import type { ConfirmationRequest, GitPanelProps, GitPanelView } from '../types/types'; +import type { ConfirmationRequest, FileOpenHandler, GitPanelView } from '../types/types'; import { getChangedFileCount } from '../utils/gitPanelUtils'; import ChangesView from '../view/changes/ChangesView'; import HistoryView from '../view/history/HistoryView'; @@ -10,13 +10,22 @@ import GitPanelHeader from '../view/GitPanelHeader'; import GitRepositoryErrorState from '../view/GitRepositoryErrorState'; import GitViewTabs from '../view/GitViewTabs'; import ConfirmActionModal from '../view/modals/ConfirmActionModal'; +import { useDeviceSettings } from '@/hooks/useDeviceSettings.js'; +import { Project } from '@/types/app.js'; -export default function GitPanel({ selectedProject, isMobile = false, onFileOpen }: GitPanelProps) { +type GitPanelProps = { + selectedProject: Project | null; + onFileOpen?: FileOpenHandler; +}; + +export default function GitPanel({ selectedProject, onFileOpen }: GitPanelProps) { const [activeView, setActiveView] = useState('changes'); const [wrapText, setWrapText] = useState(true); const [hasExpandedFiles, setHasExpandedFiles] = useState(false); const [confirmAction, setConfirmAction] = useState(null); + const { isMobile } = useDeviceSettings(); + const { gitStatus, gitDiff, diff --git a/src/components/git-panel/view/GitPanelRouterAdapter.tsx b/src/components/git-panel/view/GitPanelRouterAdapter.tsx new file mode 100644 index 00000000..a7b98a61 --- /dev/null +++ b/src/components/git-panel/view/GitPanelRouterAdapter.tsx @@ -0,0 +1,99 @@ +/** + * This is for backward compatibility with the old setup that point to the file tree route. + * It fetches the project and session data based on the URL parameters and passes them to the FileTree component. + * If no valid parameters are found, it defaults to an empty project. + * + * TODO: This adapter can be removed once all tabs use the updated projects and sessions format. + */ + +import { useParams } from "react-router-dom"; +import { useCallback, useEffect, useState } from "react"; +import { + getProjectsInLegacyFormat, + getSessionInLegacyFormat, +} from "@/components/refactored/sidebar/data/legacy-response-format-api.js"; +import { Project } from "@/types/app.js"; +import GitPanel from "@/components/git-panel/view/GitPanel.js"; +import { useSystemUI } from "@/components/refactored/shared/contexts/system-ui-context/useSystemUI"; +import type { FileDiffInfo } from "@/components/git-panel/types/types"; + +export default function GitPanelRouterAdapter() { + const { sessionId, workspaceId } = useParams<{ + sessionId?: string; + workspaceId?: string; + }>(); + + const { + codeEditorSidebar: { handleFileOpen }, + } = useSystemUI(); + + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + const fetchProjectAndSession = async () => { + setLoading(true); + + try { + if (workspaceId) { + const fetchedProject = await getProjectsInLegacyFormat(workspaceId); + + if (!cancelled) { + setProject(fetchedProject); + } + + return; + } + + if (sessionId) { + const result = await getSessionInLegacyFormat(sessionId); + + if (!cancelled) { + if (result) { + setProject(result.project); + } + } + + return; + } + + if (!cancelled) { + setProject(null); + } + } catch (error) { + console.error("Failed to fetch project/session:", error); + + if (!cancelled) { + setProject(null); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + fetchProjectAndSession(); + + return () => { + cancelled = true; + }; + }, [sessionId, workspaceId]); + + const handleProjectFileOpen = useCallback( + (filePath: string, diffInfo?: FileDiffInfo) => { + handleFileOpen(filePath, diffInfo, project?.name); + }, + [handleFileOpen, project?.name], + ); + + if (loading) { + return
Loading...
; + } + + return ( + + ); +} diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index 232f49c2..aafbf9d2 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import ChatInterface from '../../chat/view/ChatInterface'; import FileTree from '../../file-tree/view/FileTree'; import StandaloneShell from '../../standalone-shell/view/StandaloneShell'; @@ -8,13 +8,14 @@ import type { MainContentProps } from '../types/types'; import { useTaskMaster } from '../../../contexts/TaskMasterContext'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext'; import { useUiPreferences } from '../../../hooks/useUiPreferences'; -import { useEditorSidebar } from '../../../hooks/code-editor-sidebar/useEditorSidebar'; import EditorSidebar from '../../code-editor/view/EditorSidebar'; import type { Project } from '../../../types/app'; import { TaskMasterPanel } from '../../task-master'; import MainContentHeader from './subcomponents/MainContentHeader'; import MainContentStateView from './subcomponents/MainContentStateView'; import ErrorBoundary from './ErrorBoundary'; +import { useSystemUI } from '@/components/refactored/shared/contexts/system-ui-context/useSystemUI'; +import type { OpenEditorFileHandler } from '@/hooks/code-editor-sidebar/useEditorSidebar'; type TaskMasterContextValue = { currentProject?: Project | null; @@ -54,20 +55,28 @@ function MainContent({ const { currentProject, setCurrentProject } = useTaskMaster() as TaskMasterContextValue; const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings() as TasksSettingsContextValue; + const { + codeEditorSidebar: { + editingFile, + editorWidth, + editorExpanded, + hasManualWidth, + resizeHandleRef, + handleFileOpen, + handleCloseEditor, + handleToggleEditorExpand, + handleResizeStart, + }, + } = useSystemUI(); const shouldShowTasksTab = Boolean(tasksEnabled && isTaskMasterInstalled); - const { - editingFile, - editorWidth, - editorExpanded, - hasManualWidth, - resizeHandleRef, - handleFileOpen, - handleCloseEditor, - handleToggleEditorExpand, - handleResizeStart, - } = useEditorSidebar({}); + const handleProjectFileOpen = useCallback( + (filePath, diffInfo = null) => { + handleFileOpen(filePath, diffInfo, selectedProject?.name); + }, + [handleFileOpen, selectedProject?.name], + ); useEffect(() => { const selectedProjectName = selectedProject?.name; @@ -114,7 +123,7 @@ function MainContent({ ws={ws} sendMessage={sendMessage} latestMessage={latestMessage} - onFileOpen={handleFileOpen} + onFileOpen={handleProjectFileOpen} onInputFocusChange={onInputFocusChange} onSessionActive={onSessionActive} onSessionInactive={onSessionInactive} @@ -137,7 +146,7 @@ function MainContent({ {activeTab === 'files' && (
- + handleFileOpen(filePath, null, selectedProject.name)} />
)} @@ -154,7 +163,7 @@ function MainContent({ {activeTab === 'git' && (
- + handleFileOpen(filePath, diffInfo, selectedProject.name)} />
)} @@ -175,7 +184,6 @@ function MainContent({ >; isChatInputFocused: boolean; setIsChatInputFocused: Dispatch>; + codeEditorSidebar: UseEditorSidebarReturn; }; export const SystemUIContext = createContext(null); diff --git a/src/components/refactored/shared/contexts/system-ui-context/SystemUIProvider.tsx b/src/components/refactored/shared/contexts/system-ui-context/SystemUIProvider.tsx index 2cfc3ae8..7e57ea19 100644 --- a/src/components/refactored/shared/contexts/system-ui-context/SystemUIProvider.tsx +++ b/src/components/refactored/shared/contexts/system-ui-context/SystemUIProvider.tsx @@ -1,9 +1,21 @@ import { useMemo, useState, type ReactNode } from 'react'; import { SystemUIContext, type SystemUIContextValue } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIContext'; +import { useEditorSidebar } from '@/hooks/code-editor-sidebar/useEditorSidebar'; export function SystemUIProvider({ children }: { children: ReactNode }) { const [sidebarIsCollapsed, setSidebarIsCollapsed] = useState(false); const [isChatInputFocused, setIsChatInputFocused] = useState(false); + const { + editingFile, + editorWidth, + editorExpanded, + hasManualWidth, + resizeHandleRef, + handleFileOpen, + handleCloseEditor, + handleToggleEditorExpand, + handleResizeStart, + } = useEditorSidebar({}); const value = useMemo( () => ({ @@ -11,8 +23,31 @@ export function SystemUIProvider({ children }: { children: ReactNode }) { setSidebarIsCollapsed, isChatInputFocused, setIsChatInputFocused, + codeEditorSidebar: { + editingFile, + editorWidth, + editorExpanded, + hasManualWidth, + resizeHandleRef, + handleFileOpen, + handleCloseEditor, + handleToggleEditorExpand, + handleResizeStart, + }, }), - [isChatInputFocused, sidebarIsCollapsed], + [ + editingFile, + editorExpanded, + editorWidth, + handleCloseEditor, + handleFileOpen, + handleResizeStart, + handleToggleEditorExpand, + hasManualWidth, + isChatInputFocused, + resizeHandleRef, + sidebarIsCollapsed, + ], ); return ( diff --git a/src/components/refactored/shared/layout/RootLayout.tsx b/src/components/refactored/shared/layout/RootLayout.tsx index 8739248e..ca6ad9db 100644 --- a/src/components/refactored/shared/layout/RootLayout.tsx +++ b/src/components/refactored/shared/layout/RootLayout.tsx @@ -2,10 +2,13 @@ import { Outlet } from 'react-router-dom'; import { Sidebar } from '@/components/refactored/sidebar/view/Sidebar'; import { MainHeading } from '@/components/refactored/shared/layout/MainHeading'; import { MobileNav } from '@/components/refactored/shared/layout/MobileNav'; +import EditorSidebarRouterAdapter from '@/components/code-editor/view/EditorSidebarRouterAdapter'; import { useDeviceSettings } from '@/hooks/useDeviceSettings'; +import { useSystemUI } from '@/components/refactored/shared/contexts/system-ui-context/useSystemUI.js'; export function RootLayout() { const { isMobile } = useDeviceSettings({ trackPWA: false }); + const { codeEditorSidebar: { editorExpanded } } = useSystemUI(); return (
@@ -13,7 +16,12 @@ export function RootLayout() {
- +
+
+ +
+ +
diff --git a/src/hooks/code-editor-sidebar/types.ts b/src/hooks/code-editor-sidebar/types.ts index 88bf6711..61d6e4c9 100644 --- a/src/hooks/code-editor-sidebar/types.ts +++ b/src/hooks/code-editor-sidebar/types.ts @@ -7,6 +7,7 @@ export type CodeEditorDiffInfo = { export type CodeEditorFile = { name: string; path: string; + projectName?: string; diffInfo?: CodeEditorDiffInfo | null; [key: string]: unknown; -}; \ No newline at end of file +}; diff --git a/src/hooks/code-editor-sidebar/useEditorSidebar.ts b/src/hooks/code-editor-sidebar/useEditorSidebar.ts index 2c73c293..80897591 100644 --- a/src/hooks/code-editor-sidebar/useEditorSidebar.ts +++ b/src/hooks/code-editor-sidebar/useEditorSidebar.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import type { MouseEvent as ReactMouseEvent } from 'react'; +import type { MouseEvent as ReactMouseEvent, MutableRefObject } from 'react'; import { CodeEditorFile, CodeEditorDiffInfo } from '@/hooks/code-editor-sidebar/types.js'; import { useDeviceSettings } from '@/hooks/useDeviceSettings.js'; @@ -8,6 +8,24 @@ type UseEditorSidebarOptions = { initialWidth?: number; }; +export type OpenEditorFileHandler = ( + filePath: string, + diffInfo?: CodeEditorDiffInfo | null, + projectName?: string, +) => void; + +export type UseEditorSidebarReturn = { + editingFile: CodeEditorFile | null; + editorWidth: number; + editorExpanded: boolean; + hasManualWidth: boolean; + resizeHandleRef: MutableRefObject; + handleFileOpen: OpenEditorFileHandler; + handleCloseEditor: () => void; + handleToggleEditorExpand: () => void; + handleResizeStart: (event: ReactMouseEvent) => void; +}; + // TODO: Remove every parameter here (except initial width) // selectedProject is only used to set projectName on the file being edited. It turns out that projectName // isn't actually used anywhere in the code editor, so it can be removed without affecting functionality. If we do want to keep track of projectName for some reason, we can set it in the MainContent component where the file is opened instead of here. @@ -15,7 +33,7 @@ type UseEditorSidebarOptions = { // export const useEditorSidebar = ({ initialWidth = 600, -}: UseEditorSidebarOptions) => { +}: UseEditorSidebarOptions): UseEditorSidebarReturn => { const [editingFile, setEditingFile] = useState(null); const [editorWidth, setEditorWidth] = useState(initialWidth); const [editorExpanded, setEditorExpanded] = useState(false); @@ -25,14 +43,15 @@ export const useEditorSidebar = ({ const { isMobile } = useDeviceSettings({ trackPWA: false }); - const handleFileOpen = useCallback( - (filePath: string, diffInfo: CodeEditorDiffInfo | null = null) => { + const handleFileOpen = useCallback( + (filePath, diffInfo = null, projectName) => { const normalizedPath = filePath.replace(/\\/g, '/'); const fileName = normalizedPath.split('/').pop() || filePath; setEditingFile({ name: fileName, path: filePath, + projectName, diffInfo, }); }, diff --git a/src/utils/api.js b/src/utils/api.js index f563a66e..7ff31a7c 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -110,9 +110,9 @@ export const api = { body: JSON.stringify(workspaceData), }), readFile: (projectName, filePath) => - authenticatedFetch(`/api/projects/${projectName}/file?filePath=${encodeURIComponent(filePath)}`), + authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/file?filePath=${encodeURIComponent(filePath)}`), saveFile: (projectName, filePath, content) => - authenticatedFetch(`/api/projects/${projectName}/file`, { + authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/file`, { method: 'PUT', body: JSON.stringify({ filePath, content }), }),