fix: make the codeeditor part of the system ui providers

This commit is contained in:
Haileyesus
2026-04-08 16:35:08 +03:00
parent 4a4a1e1803
commit 8ed530d7cb
16 changed files with 345 additions and 45 deletions

View File

@@ -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: <Navigate to="chat" replace /> },
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
{ path: 'files', element: <FileTreeRouterAdapter /> },
{ path: 'git', element: <GitPanelRouterAdapter /> },
{ path: 'tasks', element: <TaskMasterPanel isVisible={true} /> },
{ path: ':tab', element: <WorkspaceTabRoute /> },
],
},

View File

@@ -1,3 +1,5 @@
export type { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
export type CodeEditorSettingsState = {
isDarkMode: boolean;
wordWrap: boolean;

View File

@@ -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<HTMLDivElement>(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;

View File

@@ -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<Project | null>(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 (
<EditorSidebar
editingFile={editingFile}
editorExpanded={editorExpanded}
editorWidth={editorWidth}
hasManualWidth={hasManualWidth}
resizeHandleRef={resizeHandleRef}
onResizeStart={handleResizeStart}
onCloseEditor={handleCloseEditor}
onToggleEditorExpand={handleToggleEditorExpand}
projectPath={project?.fullPath}
fillSpace={fillSpace}
/>
);
}

View File

@@ -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({

View File

@@ -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<Project | null>(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 <div>Loading...</div>;
}
console.log("FileTreeRouterAdapter project:", project);
return (
<FileTree onFileOpen={handleFileOpen} selectedProject={project} />
<FileTree onFileOpen={handleProjectFileOpen} selectedProject={project} />
);
}
}

View File

@@ -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;

View File

@@ -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<GitPanelView>('changes');
const [wrapText, setWrapText] = useState(true);
const [hasExpandedFiles, setHasExpandedFiles] = useState(false);
const [confirmAction, setConfirmAction] = useState<ConfirmationRequest | null>(null);
const { isMobile } = useDeviceSettings();
const {
gitStatus,
gitDiff,

View File

@@ -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<Project | null>(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 <div>Loading...</div>;
}
return (
<GitPanel selectedProject={project} onFileOpen={handleProjectFileOpen} />
);
}

View File

@@ -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<OpenEditorFileHandler>(
(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' && (
<div className="h-full overflow-hidden">
<FileTree selectedProject={selectedProject} onFileOpen={handleFileOpen} />
<FileTree selectedProject={selectedProject} onFileOpen={(filePath) => handleFileOpen(filePath, null, selectedProject.name)} />
</div>
)}
@@ -154,7 +163,7 @@ function MainContent({
{activeTab === 'git' && (
<div className="h-full overflow-hidden">
<GitPanel selectedProject={selectedProject} isMobile={isMobile} onFileOpen={handleFileOpen} />
<GitPanel selectedProject={selectedProject} onFileOpen={(filePath, diffInfo) => handleFileOpen(filePath, diffInfo, selectedProject.name)} />
</div>
)}
@@ -175,7 +184,6 @@ function MainContent({
<EditorSidebar
editingFile={editingFile}
isMobile={isMobile}
editorExpanded={editorExpanded}
editorWidth={editorWidth}
hasManualWidth={hasManualWidth}

View File

@@ -1,11 +1,13 @@
import { createContext } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import type { UseEditorSidebarReturn } from '@/hooks/code-editor-sidebar/useEditorSidebar';
export type SystemUIContextValue = {
sidebarIsCollapsed: boolean;
setSidebarIsCollapsed: Dispatch<SetStateAction<boolean>>;
isChatInputFocused: boolean;
setIsChatInputFocused: Dispatch<SetStateAction<boolean>>;
codeEditorSidebar: UseEditorSidebarReturn;
};
export const SystemUIContext = createContext<SystemUIContextValue | null>(null);

View File

@@ -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<SystemUIContextValue>(
() => ({
@@ -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 (

View File

@@ -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 (
<div className="flex h-screen w-full overflow-hidden bg-background">
@@ -13,7 +16,12 @@ export function RootLayout() {
<div className={`flex min-w-0 flex-1 flex-col ${isMobile ? 'pb-mobile-nav' : ''}`}>
<MainHeading />
<main className="relative min-h-0 flex-1 overflow-hidden">
<Outlet />
<div className="flex h-full min-h-0 overflow-hidden">
<div className={`flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden ${editorExpanded ? 'hidden' : ''}`}>
<Outlet />
</div>
<EditorSidebarRouterAdapter />
</div>
</main>
</div>
<MobileNav />

View File

@@ -7,6 +7,7 @@ export type CodeEditorDiffInfo = {
export type CodeEditorFile = {
name: string;
path: string;
projectName?: string;
diffInfo?: CodeEditorDiffInfo | null;
[key: string]: unknown;
};
};

View File

@@ -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<HTMLDivElement | null>;
handleFileOpen: OpenEditorFileHandler;
handleCloseEditor: () => void;
handleToggleEditorExpand: () => void;
handleResizeStart: (event: ReactMouseEvent<HTMLDivElement>) => 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<CodeEditorFile | null>(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<OpenEditorFileHandler>(
(filePath, diffInfo = null, projectName) => {
const normalizedPath = filePath.replace(/\\/g, '/');
const fileName = normalizedPath.split('/').pop() || filePath;
setEditingFile({
name: fileName,
path: filePath,
projectName,
diffInfo,
});
},

View File

@@ -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 }),
}),