mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 02:15:34 +08:00
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:
@@ -5,11 +5,17 @@ import os from 'os';
|
|||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { extractProjectDirectory } from '../../../projects.js';
|
|
||||||
import { authenticateToken } from '../auth/auth.middleware.js';
|
import { authenticateToken } from '../auth/auth.middleware.js';
|
||||||
|
|
||||||
const router = express.Router();
|
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
|
* Validate that a path is within the project root
|
||||||
* @param {string} projectRoot - The project root path
|
* @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' });
|
return res.status(400).json({ error: 'Invalid file path' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("PROJECT NAME IS: ", projectName);
|
||||||
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
||||||
|
console.log("PROJECT ROOT IS: ", projectRoot);
|
||||||
if (!projectRoot) {
|
if (!projectRoot) {
|
||||||
return res.status(404).json({ error: 'Project not found' });
|
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
|
// Use extractProjectDirectory to get the actual project path
|
||||||
let actualPath;
|
let actualPath;
|
||||||
try {
|
try {
|
||||||
|
console.log("Extracting project directory for:", req.params.projectName);
|
||||||
actualPath = await extractProjectDirectory(req.params.projectName);
|
actualPath = await extractProjectDirectory(req.params.projectName);
|
||||||
|
console.log("Extracted project directory:", actualPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error extracting project directory:', error);
|
console.error('Error extracting project directory:', error);
|
||||||
// Fallback to simple dash replacement
|
// Fallback to simple dash replacement
|
||||||
|
|||||||
@@ -2,10 +2,17 @@ import express from 'express';
|
|||||||
import spawn from 'cross-spawn';
|
import spawn from 'cross-spawn';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { extractProjectDirectory } from '../../../projects.js';
|
|
||||||
import { queryClaudeSDK } from '../../../claude-sdk.js';
|
import { queryClaudeSDK } from '../../../claude-sdk.js';
|
||||||
import { spawnCursor } from '../../../cursor-cli.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 router = express.Router();
|
||||||
const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
|
const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import spawn from 'cross-spawn';
|
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 { detectTaskMasterMCPServer } from '../../../utils/mcp-detector.js';
|
||||||
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../../../utils/taskmaster-websocket.js';
|
import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../../../utils/taskmaster-websocket.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const extractProjectDirectory = (projectName) => {
|
||||||
const __dirname = dirname(__filename);
|
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 router = express.Router();
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { WebSocketProvider } from './contexts/WebSocketContext';
|
|||||||
import i18n from './i18n/config.js';
|
import i18n from './i18n/config.js';
|
||||||
import { SystemUIProvider } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIProvider';
|
import { SystemUIProvider } from '@/components/refactored/shared/contexts/system-ui-context/SystemUIProvider';
|
||||||
import { RootLayout } from '@/components/refactored/shared/layout/RootLayout';
|
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 => {
|
const isValidRouteTab = (value: string | undefined): boolean => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -116,6 +118,8 @@ const router = createBrowserRouter(
|
|||||||
element: <WorkspaceLayout />,
|
element: <WorkspaceLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="chat" replace /> },
|
{ index: true, element: <Navigate to="chat" replace /> },
|
||||||
|
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
|
||||||
|
{ path: 'files', element: <FileTreeRouterAdapter /> },
|
||||||
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -123,6 +127,7 @@ const router = createBrowserRouter(
|
|||||||
path: 'sessions/:sessionId',
|
path: 'sessions/:sessionId',
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="chat" replace /> },
|
{ index: true, element: <Navigate to="chat" replace /> },
|
||||||
|
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
|
||||||
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { api } from '../../../utils/api';
|
import { api } from '../../../utils/api';
|
||||||
import type { CodeEditorFile } from '../types/types';
|
|
||||||
import { isBinaryFile } from '../utils/binaryFile';
|
import { isBinaryFile } from '../utils/binaryFile';
|
||||||
|
import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
|
||||||
|
|
||||||
type UseCodeEditorDocumentParams = {
|
type UseCodeEditorDocumentParams = {
|
||||||
file: CodeEditorFile;
|
file: CodeEditorFile;
|
||||||
|
|||||||
@@ -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 = {
|
export type CodeEditorSettingsState = {
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
wordWrap: boolean;
|
wordWrap: boolean;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { python } from '@codemirror/lang-python';
|
|||||||
import { getChunks } from '@codemirror/merge';
|
import { getChunks } from '@codemirror/merge';
|
||||||
import { EditorView, ViewPlugin } from '@codemirror/view';
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
||||||
import { showMinimap } from '@replit/codemirror-minimap';
|
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).
|
// Lightweight lexer for `.env` files (including `.env.*` variants).
|
||||||
const envLanguage = StreamLanguage.define({
|
const envLanguage = StreamLanguage.define({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import type { MouseEvent, MutableRefObject } from 'react';
|
import type { MouseEvent, MutableRefObject } from 'react';
|
||||||
import type { CodeEditorFile } from '../types/types';
|
|
||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from './CodeEditor';
|
||||||
|
import { CodeEditorFile } from '@/hooks/code-editor-sidebar/types.js';
|
||||||
|
|
||||||
type EditorSidebarProps = {
|
type EditorSidebarProps = {
|
||||||
editingFile: CodeEditorFile | null;
|
editingFile: CodeEditorFile | null;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CodeEditorFile } from '../../types/types';
|
import { CodeEditorFile } from "@/hooks/code-editor-sidebar/types.js";
|
||||||
|
|
||||||
type CodeEditorBinaryFileProps = {
|
type CodeEditorBinaryFileProps = {
|
||||||
file: CodeEditorFile;
|
file: CodeEditorFile;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Code2, Download, Eye, Maximize2, Minimize2, Save, Settings as SettingsIcon, X } from 'lucide-react';
|
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 = {
|
type CodeEditorHeaderProps = {
|
||||||
file: CodeEditorFile;
|
file: CodeEditorFile;
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export default function FileTree({ selectedProject, onFileOpen }: FileTreeProps)
|
|||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
const { files, loading, refreshFiles } = useFileTreeData(selectedProject);
|
const { files, loading, refreshFiles } = useFileTreeData(selectedProject);
|
||||||
|
|
||||||
|
console.log("Files are: ", files)
|
||||||
const { viewMode, changeViewMode } = useFileTreeViewMode();
|
const { viewMode, changeViewMode } = useFileTreeViewMode();
|
||||||
const { expandedDirs, toggleDirectory, expandDirectories, collapseAll } = useExpandedDirectories();
|
const { expandedDirs, toggleDirectory, expandDirectories, collapseAll } = useExpandedDirectories();
|
||||||
const { searchQuery, setSearchQuery, filteredFiles } = useFileTreeSearch({
|
const { searchQuery, setSearchQuery, filteredFiles } = useFileTreeSearch({
|
||||||
|
|||||||
93
src/components/file-tree/view/FileTreeRouterAdapter.tsx
Normal file
93
src/components/file-tree/view/FileTreeRouterAdapter.tsx
Normal 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} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import type { MainContentProps } from '../types/types';
|
|||||||
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
|
import { useTaskMaster } from '../../../contexts/TaskMasterContext';
|
||||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||||
import { useUiPreferences } from '../../../hooks/useUiPreferences';
|
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 EditorSidebar from '../../code-editor/view/EditorSidebar';
|
||||||
import type { Project } from '../../../types/app';
|
import type { Project } from '../../../types/app';
|
||||||
import { TaskMasterPanel } from '../../task-master';
|
import { TaskMasterPanel } from '../../task-master';
|
||||||
@@ -67,10 +67,7 @@ function MainContent({
|
|||||||
handleCloseEditor,
|
handleCloseEditor,
|
||||||
handleToggleEditorExpand,
|
handleToggleEditorExpand,
|
||||||
handleResizeStart,
|
handleResizeStart,
|
||||||
} = useEditorSidebar({
|
} = useEditorSidebar({});
|
||||||
selectedProject,
|
|
||||||
isMobile,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selectedProjectName = selectedProject?.name;
|
const selectedProjectName = selectedProject?.name;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/hooks/code-editor-sidebar/types.ts
Normal file
12
src/hooks/code-editor-sidebar/types.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import type { Project } from '../../../types/app';
|
import { CodeEditorFile, CodeEditorDiffInfo } from '@/hooks/code-editor-sidebar/types.js';
|
||||||
import type { CodeEditorDiffInfo, CodeEditorFile } from '../types/types';
|
import { useDeviceSettings } from '@/hooks/useDeviceSettings.js';
|
||||||
|
|
||||||
|
|
||||||
type UseEditorSidebarOptions = {
|
type UseEditorSidebarOptions = {
|
||||||
selectedProject: Project | null;
|
|
||||||
isMobile: boolean;
|
|
||||||
initialWidth?: number;
|
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 = ({
|
export const useEditorSidebar = ({
|
||||||
selectedProject,
|
|
||||||
isMobile,
|
|
||||||
initialWidth = 600,
|
initialWidth = 600,
|
||||||
}: UseEditorSidebarOptions) => {
|
}: UseEditorSidebarOptions) => {
|
||||||
const [editingFile, setEditingFile] = useState<CodeEditorFile | null>(null);
|
const [editingFile, setEditingFile] = useState<CodeEditorFile | null>(null);
|
||||||
@@ -21,6 +23,8 @@ export const useEditorSidebar = ({
|
|||||||
const [hasManualWidth, setHasManualWidth] = useState(false);
|
const [hasManualWidth, setHasManualWidth] = useState(false);
|
||||||
const resizeHandleRef = useRef<HTMLDivElement | null>(null);
|
const resizeHandleRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { isMobile } = useDeviceSettings({ trackPWA: false });
|
||||||
|
|
||||||
const handleFileOpen = useCallback(
|
const handleFileOpen = useCallback(
|
||||||
(filePath: string, diffInfo: CodeEditorDiffInfo | null = null) => {
|
(filePath: string, diffInfo: CodeEditorDiffInfo | null = null) => {
|
||||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||||
@@ -29,11 +33,10 @@ export const useEditorSidebar = ({
|
|||||||
setEditingFile({
|
setEditingFile({
|
||||||
name: fileName,
|
name: fileName,
|
||||||
path: filePath,
|
path: filePath,
|
||||||
projectName: selectedProject?.name,
|
|
||||||
diffInfo,
|
diffInfo,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[selectedProject?.name],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCloseEditor = useCallback(() => {
|
const handleCloseEditor = useCallback(() => {
|
||||||
@@ -117,29 +117,29 @@ export const api = {
|
|||||||
body: JSON.stringify({ filePath, content }),
|
body: JSON.stringify({ filePath, content }),
|
||||||
}),
|
}),
|
||||||
getFiles: (projectName, options = {}) =>
|
getFiles: (projectName, options = {}) =>
|
||||||
authenticatedFetch(`/api/projects/${projectName}/files`, options),
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files`, options),
|
||||||
|
|
||||||
// File operations
|
// File operations
|
||||||
createFile: (projectName, { path, type, name }) =>
|
createFile: (projectName, { path, type, name }) =>
|
||||||
authenticatedFetch(`/api/projects/${projectName}/files/create`, {
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files/create`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ path, type, name }),
|
body: JSON.stringify({ path, type, name }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
renameFile: (projectName, { oldPath, newName }) =>
|
renameFile: (projectName, { oldPath, newName }) =>
|
||||||
authenticatedFetch(`/api/projects/${projectName}/files/rename`, {
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files/rename`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ oldPath, newName }),
|
body: JSON.stringify({ oldPath, newName }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteFile: (projectName, { path, type }) =>
|
deleteFile: (projectName, { path, type }) =>
|
||||||
authenticatedFetch(`/api/projects/${projectName}/files`, {
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: JSON.stringify({ path, type }),
|
body: JSON.stringify({ path, type }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
uploadFiles: (projectName, formData) =>
|
uploadFiles: (projectName, formData) =>
|
||||||
authenticatedFetch(`/api/projects/${projectName}/files/upload`, {
|
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/files/upload`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {}, // Let browser set Content-Type for FormData
|
headers: {}, // Let browser set Content-Type for FormData
|
||||||
@@ -156,20 +156,20 @@ export const api = {
|
|||||||
taskmaster: {
|
taskmaster: {
|
||||||
// Initialize TaskMaster in a project
|
// Initialize TaskMaster in a project
|
||||||
init: (projectName) =>
|
init: (projectName) =>
|
||||||
authenticatedFetch(`/api/taskmaster/init/${projectName}`, {
|
authenticatedFetch(`/api/taskmaster/init/${encodeURIComponent(projectName)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Add a new task
|
// Add a new task
|
||||||
addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
|
addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
|
||||||
authenticatedFetch(`/api/taskmaster/add-task/${projectName}`, {
|
authenticatedFetch(`/api/taskmaster/add-task/${encodeURIComponent(projectName)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
|
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Parse PRD to generate tasks
|
// Parse PRD to generate tasks
|
||||||
parsePRD: (projectName, { fileName, numTasks, append }) =>
|
parsePRD: (projectName, { fileName, numTasks, append }) =>
|
||||||
authenticatedFetch(`/api/taskmaster/parse-prd/${projectName}`, {
|
authenticatedFetch(`/api/taskmaster/parse-prd/${encodeURIComponent(projectName)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ fileName, numTasks, append }),
|
body: JSON.stringify({ fileName, numTasks, append }),
|
||||||
}),
|
}),
|
||||||
@@ -180,14 +180,14 @@ export const api = {
|
|||||||
|
|
||||||
// Apply a PRD template
|
// Apply a PRD template
|
||||||
applyTemplate: (projectName, { templateId, fileName, customizations }) =>
|
applyTemplate: (projectName, { templateId, fileName, customizations }) =>
|
||||||
authenticatedFetch(`/api/taskmaster/apply-template/${projectName}`, {
|
authenticatedFetch(`/api/taskmaster/apply-template/${encodeURIComponent(projectName)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ templateId, fileName, customizations }),
|
body: JSON.stringify({ templateId, fileName, customizations }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Update a task
|
// Update a task
|
||||||
updateTask: (projectName, taskId, updates) =>
|
updateTask: (projectName, taskId, updates) =>
|
||||||
authenticatedFetch(`/api/taskmaster/update-task/${projectName}/${taskId}`, {
|
authenticatedFetch(`/api/taskmaster/update-task/${encodeURIComponent(projectName)}/${taskId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(updates),
|
body: JSON.stringify(updates),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user