diff --git a/server/src/modules/files/files.routes.js b/server/src/modules/files/files.routes.js index adb52dce..bafea5b6 100644 --- a/server/src/modules/files/files.routes.js +++ b/server/src/modules/files/files.routes.js @@ -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 diff --git a/server/src/modules/git/git.routes.js b/server/src/modules/git/git.routes.js index c5478b49..9138e221 100644 --- a/server/src/modules/git/git.routes.js +++ b/server/src/modules/git/git.routes.js @@ -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; diff --git a/server/src/modules/taskmaster/taskmaster.routes.js b/server/src/modules/taskmaster/taskmaster.routes.js index 1706b5a2..f63e1b90 100644 --- a/server/src/modules/taskmaster/taskmaster.routes.js +++ b/server/src/modules/taskmaster/taskmaster.routes.js @@ -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(); diff --git a/src/App.tsx b/src/App.tsx index 35edd914..ea38d614 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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: , children: [ { index: true, element: }, + { path: 'shell', element: }, + { path: 'files', element: }, { path: ':tab', element: }, ], }, @@ -123,6 +127,7 @@ const router = createBrowserRouter( path: 'sessions/:sessionId', children: [ { index: true, element: }, + { path: 'shell', element: }, { path: ':tab', element: }, ], }, diff --git a/src/components/code-editor/hooks/useCodeEditorDocument.ts b/src/components/code-editor/hooks/useCodeEditorDocument.ts index 5e3adc3e..adee8c45 100644 --- a/src/components/code-editor/hooks/useCodeEditorDocument.ts +++ b/src/components/code-editor/hooks/useCodeEditorDocument.ts @@ -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; diff --git a/src/components/code-editor/types/types.ts b/src/components/code-editor/types/types.ts index 8427a5e0..d2737683 100644 --- a/src/components/code-editor/types/types.ts +++ b/src/components/code-editor/types/types.ts @@ -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; diff --git a/src/components/code-editor/utils/editorExtensions.ts b/src/components/code-editor/utils/editorExtensions.ts index b98fd739..731200b5 100644 --- a/src/components/code-editor/utils/editorExtensions.ts +++ b/src/components/code-editor/utils/editorExtensions.ts @@ -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({ diff --git a/src/components/code-editor/view/EditorSidebar.tsx b/src/components/code-editor/view/EditorSidebar.tsx index 91ea9690..f170a2a3 100644 --- a/src/components/code-editor/view/EditorSidebar.tsx +++ b/src/components/code-editor/view/EditorSidebar.tsx @@ -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; diff --git a/src/components/code-editor/view/subcomponents/CodeEditorBinaryFile.tsx b/src/components/code-editor/view/subcomponents/CodeEditorBinaryFile.tsx index df58c63e..eead540f 100644 --- a/src/components/code-editor/view/subcomponents/CodeEditorBinaryFile.tsx +++ b/src/components/code-editor/view/subcomponents/CodeEditorBinaryFile.tsx @@ -1,4 +1,4 @@ -import type { CodeEditorFile } from '../../types/types'; +import { CodeEditorFile } from "@/hooks/code-editor-sidebar/types.js"; type CodeEditorBinaryFileProps = { file: CodeEditorFile; diff --git a/src/components/code-editor/view/subcomponents/CodeEditorHeader.tsx b/src/components/code-editor/view/subcomponents/CodeEditorHeader.tsx index 7c429a63..a95b1477 100644 --- a/src/components/code-editor/view/subcomponents/CodeEditorHeader.tsx +++ b/src/components/code-editor/view/subcomponents/CodeEditorHeader.tsx @@ -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; diff --git a/src/components/file-tree/view/FileTree.tsx b/src/components/file-tree/view/FileTree.tsx index e847613c..a920a4eb 100644 --- a/src/components/file-tree/view/FileTree.tsx +++ b/src/components/file-tree/view/FileTree.tsx @@ -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({ diff --git a/src/components/file-tree/view/FileTreeRouterAdapter.tsx b/src/components/file-tree/view/FileTreeRouterAdapter.tsx new file mode 100644 index 00000000..0c73422e --- /dev/null +++ b/src/components/file-tree/view/FileTreeRouterAdapter.tsx @@ -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(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
Loading...
; + } + + console.log("FileTreeRouterAdapter project:", project); + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index 89197150..232f49c2 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -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; diff --git a/src/components/refactored/sidebar/data/legacy-response-format-api.ts b/src/components/refactored/sidebar/data/legacy-response-format-api.ts new file mode 100644 index 00000000..515a69e3 --- /dev/null +++ b/src/components/refactored/sidebar/data/legacy-response-format-api.ts @@ -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).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 => { + 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; +}; diff --git a/src/components/standalone-shell/view/StandaloneShellRouterAdapter.tsx b/src/components/standalone-shell/view/StandaloneShellRouterAdapter.tsx new file mode 100644 index 00000000..62dcf726 --- /dev/null +++ b/src/components/standalone-shell/view/StandaloneShellRouterAdapter.tsx @@ -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(DEFAULT_PROJECT_FOR_EMPTY_SHELL); + const [session, setSession] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + const fetchProjectAndSession = async () => { + setLoading(true); + + try { + if (workspaceId) { + const fetchedProject = await getProjectsInLegacyFormat(workspaceId); + + if (!cancelled) { + setProject(fetchedProject ?? 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
Loading...
; + } + + return ( + + ); +} \ No newline at end of file diff --git a/src/hooks/code-editor-sidebar/types.ts b/src/hooks/code-editor-sidebar/types.ts new file mode 100644 index 00000000..88bf6711 --- /dev/null +++ b/src/hooks/code-editor-sidebar/types.ts @@ -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; +}; \ No newline at end of file diff --git a/src/components/code-editor/hooks/useEditorSidebar.ts b/src/hooks/code-editor-sidebar/useEditorSidebar.ts similarity index 81% rename from src/components/code-editor/hooks/useEditorSidebar.ts rename to src/hooks/code-editor-sidebar/useEditorSidebar.ts index d5a650b4..2c73c293 100644 --- a/src/components/code-editor/hooks/useEditorSidebar.ts +++ b/src/hooks/code-editor-sidebar/useEditorSidebar.ts @@ -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(null); @@ -21,6 +23,8 @@ export const useEditorSidebar = ({ const [hasManualWidth, setHasManualWidth] = useState(false); const resizeHandleRef = useRef(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(() => { diff --git a/src/utils/api.js b/src/utils/api.js index 51a7cca0..f563a66e 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -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), }),