+{this.state.error.message} + {this.state.error.stack && ( + <> + {"\n\nStack trace:\n"} + {this.state.error.stack} + > + )})} - ++ + +diff --git a/src/components/ProjectList.tsx b/src/components/ProjectList.tsx index 058de9604..9bef58912 100644 --- a/src/components/ProjectList.tsx +++ b/src/components/ProjectList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, memo, useCallback } from "react"; import { motion } from "framer-motion"; import { FolderOpen, @@ -55,6 +55,98 @@ const getProjectName = (path: string): string => { return parts[parts.length - 1] || path; }; +/** + * Individual project card component - Memoized to prevent unnecessary re-renders + */ +const ProjectCard = memo<{ + project: Project; + index: number; + onProjectClick: (project: Project) => void; + onProjectSettings?: (project: Project) => void; +}>(({ project, index, onProjectClick, onProjectSettings }) => { + const handleClick = useCallback(() => { + onProjectClick(project); + }, [project, onProjectClick]); + + const handleSettings = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + onProjectSettings?.(project); + }, [project, onProjectSettings]); + + return ( ++ + ); +}); + +ProjectCard.displayName = 'ProjectCard'; + /** * ProjectList component - Displays a paginated list of projects with hover animations * @@ -64,7 +156,7 @@ const getProjectName = (path: string): string => { * onProjectClick={(project) => console.log('Selected:', project)} * /> */ -export const ProjectList: React.FC+ +++++ +++ +++ {project.sessions.length > 0 && ( ++ + {getProjectName(project.path)} +
++ {project.sessions.length} + + )} ++ {project.path} +
+++++ ++++ {formatTimeAgo(project.created_at * 1000)} + +++ {project.sessions.length} + + {onProjectSettings && ( +++ + )} +e.stopPropagation()}> + + ++ ++ ++ Hooks + + = ({ +export const ProjectList: React.FC = memo(({ projects, onProjectClick, onProjectSettings, @@ -87,80 +179,13 @@ export const ProjectList: React.FC = ({ ); -}; +}); + +ProjectList.displayName = 'ProjectList'; diff --git a/src/components/SessionList.tsx b/src/components/SessionList.tsx index 7b0a2827f..02cc956b0 100644 --- a/src/components/SessionList.tsx +++ b/src/components/SessionList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, memo, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { FileText, ArrowLeft, Calendar, Clock, MessageSquare } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; @@ -38,6 +38,95 @@ interface SessionListProps { const ITEMS_PER_PAGE = 5; +/** + * Individual session card component - Memoized to prevent unnecessary re-renders + */ +const SessionCard = memo<{ + session: Session; + index: number; + projectPath: string; + onSessionClick?: (session: Session) => void; +}>(({ session, index, projectPath, onSessionClick }) => { + const handleClick = useCallback(() => { + // Emit a special event for Claude Code session navigation + const event = new CustomEvent('claude-session-selected', { + detail: { session, projectPath } + }); + window.dispatchEvent(event); + onSessionClick?.(session); + }, [session, projectPath, onSessionClick]); + + return ( +{currentProjects.map((project, index) => ( -@@ -171,4 +196,6 @@ export const ProjectList: React.FC- + project={project} + index={index} + onProjectClick={onProjectClick} + onProjectSettings={onProjectSettings} + /> ))}onProjectClick(project)} - > - ----- --- --- {project.sessions.length > 0 && ( -- - {getProjectName(project.path)} -
-- {project.sessions.length} - - )} -- {project.path} -
----- ---- {formatTimeAgo(project.created_at * 1000)} - --- {project.sessions.length} - - {onProjectSettings && ( --- - )} -e.stopPropagation()}> - - -- -{ - e.stopPropagation(); - onProjectSettings(project); - }} - > - -- Hooks - - = ({ /> + + ); +}); + +SessionCard.displayName = 'SessionCard'; + /** * SessionList component - Displays paginated sessions for a specific project * @@ -49,7 +138,7 @@ const ITEMS_PER_PAGE = 5; * onSessionClick={(session) => console.log('Selected session:', session)} * /> */ -export const SessionList: React.FC+ ++ ++++++++ ++{session.id}
+ + {/* First message preview */} + {session.first_message && ( +++ )} + + {/* Metadata */} ++++ First message: + + {truncateText(getFirstLine(session.first_message), 100)} +
++ {/* Message timestamp if available, otherwise file creation time */} ++++ + {session.todo_data && ( ++ + {session.message_timestamp + ? formatISOTimestamp(session.message_timestamp) + : formatUnixTimestamp(session.created_at) + } + + ++ )} ++ Has todo + = ({ +export const SessionList: React.FC = memo(({ sessions, projectPath, onBack, @@ -111,79 +200,13 @@ export const SessionList: React.FC = ({ @@ -195,4 +218,6 @@ export const SessionList: React.FC {currentSessions.map((session, index) => ( -- + session={session} + index={index} + projectPath={projectPath} + onSessionClick={onSessionClick} + /> ))}{ - // Emit a special event for Claude Code session navigation - const event = new CustomEvent('claude-session-selected', { - detail: { session, projectPath } - }); - window.dispatchEvent(event); - onSessionClick?.(session); - }} - > - -- -------- --{session.id}
- - {/* First message preview */} - {session.first_message && ( --- )} - - {/* Metadata */} ---- First message: - - {truncateText(getFirstLine(session.first_message), 100)} -
-- {/* Message timestamp if available, otherwise file creation time */} ---- - {session.todo_data && ( -- - {session.message_timestamp - ? formatISOTimestamp(session.message_timestamp) - : formatUnixTimestamp(session.created_at) - } - - -- )} -- Has todo - = ({ /> ); -}; \ No newline at end of file +}); + +SessionList.displayName = 'SessionList'; \ No newline at end of file diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 000000000..11442f58b --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,169 @@ +import { z } from 'zod'; + +/** + * Schema for validating file paths + * Prevents path traversal attacks and null bytes + */ +export const FilePathSchema = z.string() + .min(1, "Path cannot be empty") + .refine((path) => !path.includes('..'), "Path traversal not allowed") + .refine((path) => !path.includes('\0'), "Null bytes not allowed in path") + .refine((path) => !path.includes('~'), "Home directory shortcuts not allowed"); + +/** + * Schema for validating project paths + */ +export const ProjectPathSchema = FilePathSchema + .refine((path) => path.startsWith('/'), "Project path must be absolute"); + +/** + * Schema for validating prompts + */ +export const PromptSchema = z.string() + .min(1, "Prompt cannot be empty") + .max(10000, "Prompt is too long (max 10000 characters)") + .refine((prompt) => !prompt.includes('\0'), "Null bytes not allowed in prompt"); + +/** + * Schema for validating model names + */ +export const ModelSchema = z.enum([ + 'claude-3-opus-20240229', + 'claude-3-sonnet-20240229', + 'claude-3-haiku-20240307', + 'claude-3-5-sonnet-20241022', + 'claude-3-5-haiku-20241022' +], { + errorMap: () => ({ message: "Invalid model selected" }) +}); + +/** + * Schema for validating session IDs + */ +export const SessionIdSchema = z.string() + .uuid("Invalid session ID format"); + +/** + * Schema for Claude command execution + */ +export const ClaudeCommandSchema = z.object({ + projectPath: ProjectPathSchema, + prompt: PromptSchema, + model: ModelSchema, +}); + +/** + * Schema for resuming Claude sessions + */ +export const ResumeClaudeSchema = z.object({ + projectPath: ProjectPathSchema, + sessionId: SessionIdSchema, + prompt: PromptSchema, + model: ModelSchema, +}); + +/** + * Schema for agent creation + */ +export const AgentSchema = z.object({ + name: z.string().min(1).max(100), + icon: z.string().max(100), + system_prompt: z.string().min(1).max(5000), + default_task: z.string().max(1000).optional(), + model: ModelSchema, + enable_file_read: z.boolean(), + enable_file_write: z.boolean(), + enable_network: z.boolean(), + hooks: z.string().optional(), +}); + +/** + * Schema for file operations + */ +export const FileOperationSchema = z.object({ + filePath: FilePathSchema, + content: z.string().optional(), +}); + +/** + * Schema for directory listing + */ +export const DirectoryListSchema = z.object({ + directoryPath: FilePathSchema, +}); + +/** + * Schema for search operations + */ +export const SearchSchema = z.object({ + basePath: FilePathSchema, + query: z.string().min(1).max(100), +}); + +/** + * Validation helper functions + */ +export function validateClaudeCommand(data: unknown) { + try { + return ClaudeCommandSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Validation error: ${error.errors.map(e => e.message).join(', ')}`); + } + throw error; + } +} + +export function validateResumeCommand(data: unknown) { + try { + return ResumeClaudeSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Validation error: ${error.errors.map(e => e.message).join(', ')}`); + } + throw error; + } +} + +export function validateAgent(data: unknown) { + try { + return AgentSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Validation error: ${error.errors.map(e => e.message).join(', ')}`); + } + throw error; + } +} + +export function validateFilePath(path: string): string { + try { + return FilePathSchema.parse(path); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Invalid file path: ${error.errors[0].message}`); + } + throw error; + } +} + +export function validateProjectPath(path: string): string { + try { + return ProjectPathSchema.parse(path); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(`Invalid project path: ${error.errors[0].message}`); + } + throw error; + } +} + +/** + * Sanitize user input for display + */ +export function sanitizeForDisplay(input: string): string { + return input + .replace(/[<>]/g, '') // Remove HTML tags + .replace(/javascript:/gi, '') // Remove javascript: protocol + .trim(); +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index f7d8f21e8..64655c45a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,13 +3,13 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { AnalyticsErrorBoundary } from "./components/AnalyticsErrorBoundary"; -import { analytics, resourceMonitor } from "./lib/analytics"; +import { resourceMonitor } from "./lib/analytics"; import { PostHogProvider } from "posthog-js/react"; import "./assets/shimmer.css"; import "./styles.css"; -// Initialize analytics before rendering -analytics.initialize(); +// Analytics will be initialized after user consent +// analytics.initialize(); // Start resource monitoring (check every 2 minutes) resourceMonitor.startMonitoring(120000); @@ -20,9 +20,9 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY} options={{ api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, - defaults: '2025-05-24', capture_exceptions: true, debug: import.meta.env.MODE === "development", + opt_out_capturing_by_default: true, // Disabled by default until consent }} >