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

View File

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

View File

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

View File

@@ -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 /> },
], ],
}, },

View File

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

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 = { export type CodeEditorSettingsState = {
isDarkMode: boolean; isDarkMode: boolean;
wordWrap: boolean; wordWrap: boolean;

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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