feat: setup shell and files routes

- previously the nproject name to the backend was passed like 'C--USERS-...' and it used the legacy project-config.json file in .claude to get the original workspace paths,
However, now we are directly passing the workspace and no parsing is necessary for the routes.
So I modified the extractProjectDirectory to just return the project name.
This commit is contained in:
Haileyesus
2026-04-08 15:30:28 +03:00
parent 7c8819cf34
commit 4a4a1e1803
18 changed files with 378 additions and 50 deletions

View File

@@ -5,11 +5,17 @@ import os from 'os';
import mime from 'mime-types';
import fetch from 'node-fetch';
import { promises as fsPromises } from 'fs';
import { extractProjectDirectory } from '../../../projects.js';
import { authenticateToken } from '../auth/auth.middleware.js';
const router = express.Router();
const extractProjectDirectory = (projectName) => {
return new Promise((resolve, reject) => {
// just return the original project name for now, since we are no longer encoding the path in the project name
resolve(projectName);
});
}
/**
* Validate that a path is within the project root
* @param {string} projectRoot - The project root path
@@ -149,7 +155,9 @@ router.get('/api/projects/:projectName/file', authenticateToken, async (req, res
return res.status(400).json({ error: 'Invalid file path' });
}
console.log("PROJECT NAME IS: ", projectName);
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
console.log("PROJECT ROOT IS: ", projectRoot);
if (!projectRoot) {
return res.status(404).json({ error: 'Project not found' });
}
@@ -288,7 +296,9 @@ router.get('/api/projects/:projectName/files', authenticateToken, async (req, re
// Use extractProjectDirectory to get the actual project path
let actualPath;
try {
console.log("Extracting project directory for:", req.params.projectName);
actualPath = await extractProjectDirectory(req.params.projectName);
console.log("Extracted project directory:", actualPath);
} catch (error) {
console.error('Error extracting project directory:', error);
// Fallback to simple dash replacement

View File

@@ -2,10 +2,17 @@ import express from 'express';
import spawn from 'cross-spawn';
import path from 'path';
import { promises as fs } from 'fs';
import { extractProjectDirectory } from '../../../projects.js';
import { queryClaudeSDK } from '../../../claude-sdk.js';
import { spawnCursor } from '../../../cursor-cli.js';
const extractProjectDirectory = (projectName) => {
return new Promise((resolve, reject) => {
// just return the original project name for now, since we are no longer encoding the path in the project name
resolve(projectName);
});
}
const router = express.Router();
const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;

View File

@@ -13,15 +13,17 @@ import fs from 'fs';
import path from 'path';
import { promises as fsPromises } from 'fs';
import spawn from 'cross-spawn';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import os from 'os';
import { extractProjectDirectory } from '../../../projects.js';
import { detectTaskMasterMCPServer } from '../../../utils/mcp-detector.js';
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../../../utils/taskmaster-websocket.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const extractProjectDirectory = (projectName) => {
return new Promise((resolve, reject) => {
// just return the original project name for now, since we are no longer encoding the path in the project name
resolve(projectName);
});
}
const router = express.Router();

View File

@@ -16,6 +16,8 @@ import { WebSocketProvider } from './contexts/WebSocketContext';
import i18n from './i18n/config.js';
import { SystemUIProvider } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIProvider';
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';
const isValidRouteTab = (value: string | undefined): boolean => {
if (!value) {
@@ -116,6 +118,8 @@ const router = createBrowserRouter(
element: <WorkspaceLayout />,
children: [
{ index: true, element: <Navigate to="chat" replace /> },
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
{ path: 'files', element: <FileTreeRouterAdapter /> },
{ path: ':tab', element: <WorkspaceTabRoute /> },
],
},
@@ -123,6 +127,7 @@ const router = createBrowserRouter(
path: 'sessions/:sessionId',
children: [
{ index: true, element: <Navigate to="chat" replace /> },
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
{ path: ':tab', element: <WorkspaceTabRoute /> },
],
},

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { api } from '../../../utils/api';
import type { CodeEditorFile } from '../types/types';
import { isBinaryFile } from '../utils/binaryFile';
import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
type UseCodeEditorDocumentParams = {
file: CodeEditorFile;

View File

@@ -1,17 +1,3 @@
export type CodeEditorDiffInfo = {
old_string?: string;
new_string?: string;
[key: string]: unknown;
};
export type CodeEditorFile = {
name: string;
path: string;
projectName?: string;
diffInfo?: CodeEditorDiffInfo | null;
[key: string]: unknown;
};
export type CodeEditorSettingsState = {
isDarkMode: boolean;
wordWrap: boolean;

View File

@@ -8,7 +8,7 @@ import { python } from '@codemirror/lang-python';
import { getChunks } from '@codemirror/merge';
import { EditorView, ViewPlugin } from '@codemirror/view';
import { showMinimap } from '@replit/codemirror-minimap';
import type { CodeEditorFile } from '../types/types';
import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
// Lightweight lexer for `.env` files (including `.env.*` variants).
const envLanguage = StreamLanguage.define({

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import type { MouseEvent, MutableRefObject } from 'react';
import type { CodeEditorFile } from '../types/types';
import CodeEditor from './CodeEditor';
import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
type EditorSidebarProps = {
editingFile: CodeEditorFile | null;

View File

@@ -1,4 +1,4 @@
import type { CodeEditorFile } from '../../types/types';
import { CodeEditorFile } from "@/hooks/code-editor-sidebar/types.js";
type CodeEditorBinaryFileProps = {
file: CodeEditorFile;

View File

@@ -1,5 +1,5 @@
import { Code2, Download, Eye, Maximize2, Minimize2, Save, Settings as SettingsIcon, X } from 'lucide-react';
import type { CodeEditorFile } from '../../types/types';
import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
type CodeEditorHeaderProps = {
file: CodeEditorFile;

View File

@@ -46,6 +46,8 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
}, [toast]);
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

@@ -0,0 +1,93 @@
/**
* 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 { 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";
export default function FileTreeRouterAdapter() {
const { sessionId, workspaceId } = useParams<{
sessionId?: string;
workspaceId?: string;
}>();
const { handleFileOpen } = useEditorSidebar({});
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]);
console.log("FileTreeRouterAdapter project:", project);
if (loading) {
return <div>Loading...</div>;
}
console.log("FileTreeRouterAdapter project:", project);
return (
<FileTree onFileOpen={handleFileOpen} selectedProject={project} />
);
}

View File

@@ -8,7 +8,7 @@ import type { MainContentProps } from '../types/types';
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
import { useUiPreferences } from '../../../hooks/useUiPreferences';
import { useEditorSidebar } from '../../code-editor/hooks/useEditorSidebar';
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';
@@ -67,10 +67,7 @@ function MainContent({
handleCloseEditor,
handleToggleEditorExpand,
handleResizeStart,
} = useEditorSidebar({
selectedProject,
isMobile,
});
} = useEditorSidebar({});
useEffect(() => {
const selectedProjectName = selectedProject?.name;

View File

@@ -0,0 +1,111 @@
// TODO: Remove this legacy response adapter once all consumers are migrated to the workspaces API shape.
import type { Project, ProjectSession, SessionProvider } from '@/types/app';
import { getWorkspaceSessions } from '@/components/refactored/sidebar/data/workspacesApi';
import type { WorkspaceRecord, WorkspaceSession } from '@/components/refactored/sidebar/types';
const readString = (value: unknown): string | null =>
typeof value === 'string' ? value : null;
const readNumber = (value: unknown): number | null =>
typeof value === 'number' ? value : null;
const toLegacySession = (
session: WorkspaceSession,
projectName: string | null,
): ProjectSession => {
const id = readString(session.id) ?? readString(session.sessionId);
const summary = readString(session.summary);
const name = readString(session.customName) ?? summary;
return {
id,
title: name,
summary,
name,
createdAt: readString(session.createdAt),
created_at: readString(session.createdAt),
updated_at: readString(session.updatedAt),
lastActivity: readString(session.lastActivity),
messageCount: readNumber((session as Record<string, unknown>).messageCount),
__provider: (readString(session.provider) as SessionProvider | null) ?? null,
__projectName: projectName,
} as unknown as ProjectSession;
};
const mapWorkspaceToLegacyProject = (workspace: WorkspaceRecord): Project => {
const projectName =
readString(workspace.workspaceOriginalPath) ??
readString(workspace.workspaceDisplayName);
const legacySessions = workspace.sessions.map((session) =>
toLegacySession(session, projectName),
);
const claudeSessions = legacySessions.filter(
(session) => session.__provider === 'claude',
);
const cursorSessions = legacySessions.filter(
(session) => session.__provider === 'cursor',
);
const codexSessions = legacySessions.filter(
(session) => session.__provider === 'codex',
);
const geminiSessions = legacySessions.filter(
(session) => session.__provider === 'gemini',
);
return {
name: projectName,
displayName:
readString(workspace.workspaceCustomName) ??
readString(workspace.workspaceDisplayName),
fullPath: readString(workspace.workspaceOriginalPath),
path: readString(workspace.workspaceOriginalPath),
sessions: claudeSessions.length > 0 ? claudeSessions : null,
cursorSessions: cursorSessions.length > 0 ? cursorSessions : null,
codexSessions: codexSessions.length > 0 ? codexSessions : null,
geminiSessions: geminiSessions.length > 0 ? geminiSessions : null,
sessionMeta: null,
taskmaster: null,
} as unknown as Project;
};
export const getProjectsInLegacyFormat = async (
workspaceId: string,
): Promise<Project | null> => {
const workspaces = await getWorkspaceSessions();
const workspace = workspaces.find(
(workspaceRecord) => workspaceRecord.workspaceId === workspaceId,
);
if (!workspace) {
return null;
}
return mapWorkspaceToLegacyProject(workspace);
};
export const getSessionInLegacyFormat = async (
sessionId: string,
): Promise<{ project: Project; session: ProjectSession } | null> => {
const workspaces = await getWorkspaceSessions();
for (const workspace of workspaces) {
const legacyProject = mapWorkspaceToLegacyProject(workspace);
const projectName =
readString(workspace.workspaceOriginalPath) ??
readString(workspace.workspaceDisplayName);
const matchedSession = workspace.sessions.find(
(session) => session.sessionId === sessionId || session.id === sessionId,
);
if (matchedSession) {
return {
project: legacyProject,
session: toLegacySession(matchedSession, projectName),
};
}
}
return null;
};

View File

@@ -0,0 +1,100 @@
/**
* This is for backward compatibility with the old setup that point to the standalone shell.
* It fetches the project and session data based on the URL parameters and passes them to the StandaloneShell 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 { useEffect, useState } from "react";
import StandaloneShell from "@/components/standalone-shell/view/StandaloneShell.js";
import {
getProjectsInLegacyFormat,
getSessionInLegacyFormat,
} from "@/components/refactored/sidebar/data/legacy-response-format-api.js";
import { DEFAULT_PROJECT_FOR_EMPTY_SHELL } from "@/constants/config.js";
import { Project, ProjectSession } from "@/types/app.js";
export default function StandaloneShellRouterAdapter() {
const { sessionId, workspaceId } = useParams<{
sessionId?: string;
workspaceId?: string;
}>();
const [project, setProject] = useState<Project | null>(DEFAULT_PROJECT_FOR_EMPTY_SHELL);
const [session, setSession] = useState<ProjectSession | 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 ?? DEFAULT_PROJECT_FOR_EMPTY_SHELL);
setSession(null);
}
return;
}
if (sessionId) {
const result = await getSessionInLegacyFormat(sessionId);
if (!cancelled) {
if (result) {
setProject(result.project ?? DEFAULT_PROJECT_FOR_EMPTY_SHELL);
setSession(result.session ?? null);
} else {
setProject(DEFAULT_PROJECT_FOR_EMPTY_SHELL);
setSession(null);
}
}
return;
}
if (!cancelled) {
setProject(DEFAULT_PROJECT_FOR_EMPTY_SHELL);
setSession(null);
}
} catch (error) {
console.error("Failed to fetch project/session:", error);
if (!cancelled) {
setProject(DEFAULT_PROJECT_FOR_EMPTY_SHELL);
setSession(null);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchProjectAndSession();
return () => {
cancelled = true;
};
}, [sessionId, workspaceId]);
if (loading) {
return <div>Loading...</div>;
}
return (
<StandaloneShell
project={project}
session={session}
isActive={true}
showHeader={false}
/>
);
}

View File

@@ -0,0 +1,12 @@
export type CodeEditorDiffInfo = {
old_string?: string;
new_string?: string;
[key: string]: unknown;
};
export type CodeEditorFile = {
name: string;
path: string;
diffInfo?: CodeEditorDiffInfo | null;
[key: string]: unknown;
};

View File

@@ -1,17 +1,19 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { Project } from '../../../types/app';
import type { CodeEditorDiffInfo, CodeEditorFile } from '../types/types';
import { CodeEditorFile, CodeEditorDiffInfo } from '@/hooks/code-editor-sidebar/types.js';
import { useDeviceSettings } from '@/hooks/useDeviceSettings.js';
type UseEditorSidebarOptions = {
selectedProject: Project | null;
isMobile: boolean;
initialWidth?: number;
};
// 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.
// isMobile should be found from useDeviceSettings hook
//
export const useEditorSidebar = ({
selectedProject,
isMobile,
initialWidth = 600,
}: UseEditorSidebarOptions) => {
const [editingFile, setEditingFile] = useState<CodeEditorFile | null>(null);
@@ -21,6 +23,8 @@ export const useEditorSidebar = ({
const [hasManualWidth, setHasManualWidth] = useState(false);
const resizeHandleRef = useRef<HTMLDivElement | null>(null);
const { isMobile } = useDeviceSettings({ trackPWA: false });
const handleFileOpen = useCallback(
(filePath: string, diffInfo: CodeEditorDiffInfo | null = null) => {
const normalizedPath = filePath.replace(/\\/g, '/');
@@ -29,11 +33,10 @@ export const useEditorSidebar = ({
setEditingFile({
name: fileName,
path: filePath,
projectName: selectedProject?.name,
diffInfo,
});
},
[selectedProject?.name],
[],
);
const handleCloseEditor = useCallback(() => {

View File

@@ -117,29 +117,29 @@ export const api = {
body: JSON.stringify({ filePath, content }),
}),
getFiles: (projectName, options = {}) =>
authenticatedFetch(`/api/projects/${projectName}/files`, options),
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files`, options),
// File operations
createFile: (projectName, { path, type, name }) =>
authenticatedFetch(`/api/projects/${projectName}/files/create`, {
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files/create`, {
method: 'POST',
body: JSON.stringify({ path, type, name }),
}),
renameFile: (projectName, { oldPath, newName }) =>
authenticatedFetch(`/api/projects/${projectName}/files/rename`, {
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files/rename`, {
method: 'PUT',
body: JSON.stringify({ oldPath, newName }),
}),
deleteFile: (projectName, { path, type }) =>
authenticatedFetch(`/api/projects/${projectName}/files`, {
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files`, {
method: 'DELETE',
body: JSON.stringify({ path, type }),
}),
uploadFiles: (projectName, formData) =>
authenticatedFetch(`/api/projects/${projectName}/files/upload`, {
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files/upload`, {
method: 'POST',
body: formData,
headers: {}, // Let browser set Content-Type for FormData
@@ -156,20 +156,20 @@ export const api = {
taskmaster: {
// Initialize TaskMaster in a project
init: (projectName) =>
authenticatedFetch(`/api/taskmaster/init/${projectName}`, {
authenticatedFetch(`/api/taskmaster/init/${encodeURIComponent(projectName)}`, {
method: 'POST',
}),
// Add a new task
addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
authenticatedFetch(`/api/taskmaster/add-task/${projectName}`, {
authenticatedFetch(`/api/taskmaster/add-task/${encodeURIComponent(projectName)}`, {
method: 'POST',
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
}),
// Parse PRD to generate tasks
parsePRD: (projectName, { fileName, numTasks, append }) =>
authenticatedFetch(`/api/taskmaster/parse-prd/${projectName}`, {
authenticatedFetch(`/api/taskmaster/parse-prd/${encodeURIComponent(projectName)}`, {
method: 'POST',
body: JSON.stringify({ fileName, numTasks, append }),
}),
@@ -180,14 +180,14 @@ export const api = {
// Apply a PRD template
applyTemplate: (projectName, { templateId, fileName, customizations }) =>
authenticatedFetch(`/api/taskmaster/apply-template/${projectName}`, {
authenticatedFetch(`/api/taskmaster/apply-template/${encodeURIComponent(projectName)}`, {
method: 'POST',
body: JSON.stringify({ templateId, fileName, customizations }),
}),
// Update a task
updateTask: (projectName, taskId, updates) =>
authenticatedFetch(`/api/taskmaster/update-task/${projectName}/${taskId}`, {
authenticatedFetch(`/api/taskmaster/update-task/${encodeURIComponent(projectName)}/${taskId}`, {
method: 'PUT',
body: JSON.stringify(updates),
}),