From 845d5346eb383ebd9ec03e876f8fa35c345ea0b0 Mon Sep 17 00:00:00 2001 From: Simos Date: Thu, 3 Jul 2025 23:15:36 +0200 Subject: [PATCH 1/3] Update file permissions to executable for multiple files and add Dark Mode toggle functionality with theme context management. Introduce Quick Settings Panel for user preferences and enhance project display name generation in server logic. --- .claude/settings.local.json | 17 ++ .gitignore | 0 LICENSE | 0 README.md | 0 index.html | 0 package-lock.json | 0 package.json | 0 postcss.config.js | 0 public/convert-icons.md | 0 public/favicon.png | Bin public/favicon.svg | 0 public/generate-icons.js | 0 public/icons/generate-icons.md | 0 public/icons/icon-128x128.png | Bin public/icons/icon-128x128.svg | 0 public/icons/icon-144x144.png | Bin public/icons/icon-144x144.svg | 0 public/icons/icon-152x152.png | Bin public/icons/icon-152x152.svg | 0 public/icons/icon-192x192.png | Bin public/icons/icon-192x192.svg | 0 public/icons/icon-384x384.png | Bin public/icons/icon-384x384.svg | 0 public/icons/icon-512x512.png | Bin public/icons/icon-512x512.svg | 0 public/icons/icon-72x72.png | Bin public/icons/icon-72x72.svg | 0 public/icons/icon-96x96.png | Bin public/icons/icon-96x96.svg | 0 public/icons/icon-template.svg | 0 public/logo.svg | 0 public/manifest.json | 0 public/screenshots/desktop-main.png | Bin public/screenshots/mobile-chat.png | Bin public/screenshots/tools-modal.png | Bin public/sw.js | 0 server/claude-cli.js | 0 server/claude-cli.js.backup.1750077611635 | 0 server/index.js | 0 server/projects.js | 32 ++-- src/App.jsx | 47 +++++- src/components/ChatInterface.jsx | 135 +++++++++++----- src/components/CodeEditor.jsx | 0 src/components/DarkModeToggle.jsx | 35 +++++ src/components/FileTree.jsx | 0 src/components/ImageViewer.jsx | 0 src/components/MainContent.jsx | 9 +- src/components/MobileNav.jsx | 0 src/components/QuickSettingsPanel.jsx | 179 ++++++++++++++++++++++ src/components/Shell.jsx | 0 src/components/Sidebar.jsx | 0 src/components/TodoList.jsx | 24 +-- src/components/ToolsSettings.jsx | 46 +++++- src/components/ui/badge.jsx | 0 src/components/ui/button.jsx | 0 src/components/ui/input.jsx | 0 src/components/ui/scroll-area.jsx | 0 src/contexts/ThemeContext.jsx | 72 +++++++++ src/index.css | 66 ++++---- src/lib/utils.js | 0 src/main.jsx | 0 src/utils/websocket.js | 0 tailwind.config.js | 0 vite.config.js | 0 64 files changed, 562 insertions(+), 100 deletions(-) create mode 100755 .claude/settings.local.json mode change 100644 => 100755 .gitignore mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 index.html mode change 100644 => 100755 package-lock.json mode change 100644 => 100755 package.json mode change 100644 => 100755 postcss.config.js mode change 100644 => 100755 public/convert-icons.md mode change 100644 => 100755 public/favicon.png mode change 100644 => 100755 public/favicon.svg mode change 100644 => 100755 public/generate-icons.js mode change 100644 => 100755 public/icons/generate-icons.md mode change 100644 => 100755 public/icons/icon-128x128.png mode change 100644 => 100755 public/icons/icon-128x128.svg mode change 100644 => 100755 public/icons/icon-144x144.png mode change 100644 => 100755 public/icons/icon-144x144.svg mode change 100644 => 100755 public/icons/icon-152x152.png mode change 100644 => 100755 public/icons/icon-152x152.svg mode change 100644 => 100755 public/icons/icon-192x192.png mode change 100644 => 100755 public/icons/icon-192x192.svg mode change 100644 => 100755 public/icons/icon-384x384.png mode change 100644 => 100755 public/icons/icon-384x384.svg mode change 100644 => 100755 public/icons/icon-512x512.png mode change 100644 => 100755 public/icons/icon-512x512.svg mode change 100644 => 100755 public/icons/icon-72x72.png mode change 100644 => 100755 public/icons/icon-72x72.svg mode change 100644 => 100755 public/icons/icon-96x96.png mode change 100644 => 100755 public/icons/icon-96x96.svg mode change 100644 => 100755 public/icons/icon-template.svg mode change 100644 => 100755 public/logo.svg mode change 100644 => 100755 public/manifest.json mode change 100644 => 100755 public/screenshots/desktop-main.png mode change 100644 => 100755 public/screenshots/mobile-chat.png mode change 100644 => 100755 public/screenshots/tools-modal.png mode change 100644 => 100755 public/sw.js mode change 100644 => 100755 server/claude-cli.js mode change 100644 => 100755 server/claude-cli.js.backup.1750077611635 mode change 100644 => 100755 server/index.js mode change 100644 => 100755 server/projects.js mode change 100644 => 100755 src/App.jsx mode change 100644 => 100755 src/components/ChatInterface.jsx mode change 100644 => 100755 src/components/CodeEditor.jsx create mode 100755 src/components/DarkModeToggle.jsx mode change 100644 => 100755 src/components/FileTree.jsx mode change 100644 => 100755 src/components/ImageViewer.jsx mode change 100644 => 100755 src/components/MainContent.jsx mode change 100644 => 100755 src/components/MobileNav.jsx create mode 100755 src/components/QuickSettingsPanel.jsx mode change 100644 => 100755 src/components/Shell.jsx mode change 100644 => 100755 src/components/Sidebar.jsx mode change 100644 => 100755 src/components/TodoList.jsx mode change 100644 => 100755 src/components/ToolsSettings.jsx mode change 100644 => 100755 src/components/ui/badge.jsx mode change 100644 => 100755 src/components/ui/button.jsx mode change 100644 => 100755 src/components/ui/input.jsx mode change 100644 => 100755 src/components/ui/scroll-area.jsx create mode 100755 src/contexts/ThemeContext.jsx mode change 100644 => 100755 src/index.css mode change 100644 => 100755 src/lib/utils.js mode change 100644 => 100755 src/main.jsx mode change 100644 => 100755 src/utils/websocket.js mode change 100644 => 100755 tailwind.config.js mode change 100644 => 100755 vite.config.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100755 index 00000000..63ff59d3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(git init:*)", + "Bash(mkdir:*)", + "Bash(git commit:*)", + "Bash(git push:*)", + "Bash(rg:*)", + "Bash(sed:*)", + "Bash(grep:*)", + "Bash(timeout:*)", + "Bash(curl:*)", + "Bash(npm install:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/index.html b/index.html old mode 100644 new mode 100755 diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/postcss.config.js b/postcss.config.js old mode 100644 new mode 100755 diff --git a/public/convert-icons.md b/public/convert-icons.md old mode 100644 new mode 100755 diff --git a/public/favicon.png b/public/favicon.png old mode 100644 new mode 100755 diff --git a/public/favicon.svg b/public/favicon.svg old mode 100644 new mode 100755 diff --git a/public/generate-icons.js b/public/generate-icons.js old mode 100644 new mode 100755 diff --git a/public/icons/generate-icons.md b/public/icons/generate-icons.md old mode 100644 new mode 100755 diff --git a/public/icons/icon-128x128.png b/public/icons/icon-128x128.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-128x128.svg b/public/icons/icon-128x128.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-144x144.png b/public/icons/icon-144x144.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-144x144.svg b/public/icons/icon-144x144.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-152x152.png b/public/icons/icon-152x152.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-152x152.svg b/public/icons/icon-152x152.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-192x192.png b/public/icons/icon-192x192.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-192x192.svg b/public/icons/icon-192x192.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-384x384.png b/public/icons/icon-384x384.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-384x384.svg b/public/icons/icon-384x384.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-512x512.png b/public/icons/icon-512x512.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-512x512.svg b/public/icons/icon-512x512.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-72x72.png b/public/icons/icon-72x72.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-72x72.svg b/public/icons/icon-72x72.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-96x96.png b/public/icons/icon-96x96.png old mode 100644 new mode 100755 diff --git a/public/icons/icon-96x96.svg b/public/icons/icon-96x96.svg old mode 100644 new mode 100755 diff --git a/public/icons/icon-template.svg b/public/icons/icon-template.svg old mode 100644 new mode 100755 diff --git a/public/logo.svg b/public/logo.svg old mode 100644 new mode 100755 diff --git a/public/manifest.json b/public/manifest.json old mode 100644 new mode 100755 diff --git a/public/screenshots/desktop-main.png b/public/screenshots/desktop-main.png old mode 100644 new mode 100755 diff --git a/public/screenshots/mobile-chat.png b/public/screenshots/mobile-chat.png old mode 100644 new mode 100755 diff --git a/public/screenshots/tools-modal.png b/public/screenshots/tools-modal.png old mode 100644 new mode 100755 diff --git a/public/sw.js b/public/sw.js old mode 100644 new mode 100755 diff --git a/server/claude-cli.js b/server/claude-cli.js old mode 100644 new mode 100755 diff --git a/server/claude-cli.js.backup.1750077611635 b/server/claude-cli.js.backup.1750077611635 old mode 100644 new mode 100755 diff --git a/server/index.js b/server/index.js old mode 100644 new mode 100755 diff --git a/server/projects.js b/server/projects.js old mode 100644 new mode 100755 index b1597c09..3f96eebb --- a/server/projects.js +++ b/server/projects.js @@ -21,23 +21,37 @@ async function saveProjectConfig(config) { } // Generate better display name from path -function generateDisplayName(projectName) { +async function generateDisplayName(projectName) { // Convert "-home-user-projects-myapp" to a readable format - let path = projectName.replace(/-/g, '/'); + let projectPath = projectName.replace(/-/g, '/'); + + // Try to read package.json from the project path + try { + const packageJsonPath = path.join(projectPath, 'package.json'); + const packageData = await fs.readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageData); + + // Return the name from package.json if it exists + if (packageJson.name) { + return packageJson.name; + } + } catch (error) { + // Fall back to path-based naming if package.json doesn't exist or can't be read + } // If it starts with /, it's an absolute path - if (path.startsWith('/')) { - const parts = path.split('/').filter(Boolean); + if (projectPath.startsWith('/')) { + const parts = projectPath.split('/').filter(Boolean); if (parts.length > 3) { // Show last 2 folders with ellipsis: "...projects/myapp" return `.../${parts.slice(-2).join('/')}`; } else { // Show full path if short: "/home/user" - return path; + return projectPath; } } - return path; + return projectPath; } async function getProjects() { @@ -57,7 +71,7 @@ async function getProjects() { // Get display name from config or generate one const customName = config[entry.name]?.displayName; - const autoDisplayName = generateDisplayName(entry.name); + const autoDisplayName = await generateDisplayName(entry.name); const fullPath = entry.name.replace(/-/g, '/'); const project = { @@ -96,7 +110,7 @@ async function getProjects() { const project = { name: projectName, path: null, // No physical path yet - displayName: projectConfig.displayName || generateDisplayName(projectName), + displayName: projectConfig.displayName || await generateDisplayName(projectName), fullPath: fullPath, isCustomName: !!projectConfig.displayName, isManuallyAdded: true, @@ -451,7 +465,7 @@ async function addProjectManually(projectPath, displayName = null) { name: projectName, path: null, fullPath: absolutePath, - displayName: displayName || generateDisplayName(projectName), + displayName: displayName || await generateDisplayName(projectName), isManuallyAdded: true, sessions: [] }; diff --git a/src/App.jsx b/src/App.jsx old mode 100644 new mode 100755 index 7888a516..dcfac641 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,7 +24,11 @@ import Sidebar from './components/Sidebar'; import MainContent from './components/MainContent'; import MobileNav from './components/MobileNav'; import ToolsSettings from './components/ToolsSettings'; +import QuickSettingsPanel from './components/QuickSettingsPanel'; + import { useWebSocket } from './utils/websocket'; +import { ThemeProvider } from './contexts/ThemeContext'; + // Main App component with routing function AppContent() { @@ -40,6 +44,15 @@ function AppContent() { const [isLoadingProjects, setIsLoadingProjects] = useState(true); const [isInputFocused, setIsInputFocused] = useState(false); const [showToolsSettings, setShowToolsSettings] = useState(false); + const [showQuickSettings, setShowQuickSettings] = useState(false); + const [autoExpandTools, setAutoExpandTools] = useState(() => { + const saved = localStorage.getItem('autoExpandTools'); + return saved !== null ? JSON.parse(saved) : false; + }); + const [showRawParameters, setShowRawParameters] = useState(() => { + const saved = localStorage.getItem('showRawParameters'); + return saved !== null ? JSON.parse(saved) : false; + }); // Session Protection System: Track sessions with active conversations to prevent // automatic project updates from interrupting ongoing chats. When a user sends // a message, the session is marked as "active" and project updates are paused @@ -455,6 +468,9 @@ function AppContent() { onSessionInactive={markSessionAsInactive} onReplaceTemporarySession={replaceTemporarySession} onNavigateToSession={(sessionId) => navigate(`/session/${sessionId}`)} + onShowSettings={() => setShowToolsSettings(true)} + autoExpandTools={autoExpandTools} + showRawParameters={showRawParameters} /> @@ -466,6 +482,23 @@ function AppContent() { isInputFocused={isInputFocused} /> )} + + {/* Quick Settings Panel */} + { + setAutoExpandTools(value); + localStorage.setItem('autoExpandTools', JSON.stringify(value)); + }} + showRawParameters={showRawParameters} + onShowRawParametersChange={(value) => { + setShowRawParameters(value); + localStorage.setItem('showRawParameters', JSON.stringify(value)); + }} + isMobile={isMobile} + /> {/* Tools Settings Modal */} - - } /> - } /> - - + + + + } /> + } /> + + + ); } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx old mode 100644 new mode 100755 index 140b3ab0..e89cff06 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -21,13 +21,45 @@ import ReactMarkdown from 'react-markdown'; import TodoList from './TodoList'; // Memoized message component to prevent unnecessary re-renders -const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen }) => { +const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => { const isGrouped = prevMessage && prevMessage.type === message.type && prevMessage.type === 'assistant' && !prevMessage.isToolUse && !message.isToolUse; - + + const messageRef = React.useRef(null); + const [isExpanded, setIsExpanded] = React.useState(false); + + React.useEffect(() => { + if (!autoExpandTools || !messageRef.current || !message.isToolUse) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && !isExpanded) { + setIsExpanded(true); + // Find all details elements and open them + const details = messageRef.current.querySelectorAll('details'); + details.forEach(detail => { + detail.open = true; + }); + } + }); + }, + { threshold: 0.1 } + ); + + observer.observe(messageRef.current); + + return () => { + if (messageRef.current) { + observer.unobserve(messageRef.current); + } + }; + }, [autoExpandTools, isExpanded, message.isToolUse]); + return (
{message.type === 'user' ? ( @@ -71,26 +103,43 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile {message.isToolUse ? (
-
-
- - - - +
+
+
+ + + + +
+ + Using {message.toolName} + + + {message.toolId} +
- - Using {message.toolName} - - - {message.toolId} - + {onShowSettings && ( + + )}
{message.toolInput && message.toolName === 'Edit' && (() => { try { const input = JSON.parse(message.toolInput); if (input.file_path && input.old_string && input.new_string) { return ( -
+
@@ -146,15 +195,17 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
))}
-
-
- - View raw parameters - -
-                                {message.toolInput}
-                              
-
+
{showRawParameters && ( +
+ + View raw parameters + +
+                                  {message.toolInput}
+                                
+
+ )} + ); @@ -163,7 +214,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile // Fall back to raw display if parsing fails } return ( -
+
View input parameters @@ -180,7 +231,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile const input = JSON.parse(message.toolInput); if (input.todos && Array.isArray(input.todos)) { return ( -
+
@@ -189,15 +240,18 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
-
- - View raw parameters - -
-                                  {message.toolInput}
-                                
-
+ {showRawParameters && ( +
+ + View raw parameters + +
+                                    {message.toolInput}
+                                  
+
+ )}
+
); } @@ -208,7 +262,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile // Regular tool input display for other tools return ( -
+
@@ -311,7 +365,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile if (content.includes('cat -n') && content.includes('→')) { return ( -
+
@@ -329,7 +383,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile if (content.length > 300) { return ( -
+
@@ -418,7 +472,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile // - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID // // This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations. -function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession }) { +function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters }) { const [input, setInput] = useState(() => { if (typeof window !== 'undefined' && selectedProject) { return localStorage.getItem(`draft_input_${selectedProject.name}`) || ''; @@ -451,6 +505,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const [canAbortSession, setCanAbortSession] = useState(false); const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); + // Memoized diff calculation to prevent recalculating on every render const createDiff = useMemo(() => { const cache = new Map(); @@ -1254,6 +1309,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess prevMessage={prevMessage} createDiff={createDiff} onFileOpen={onFileOpen} + onShowSettings={onShowSettings} + autoExpandTools={autoExpandTools} + showRawParameters={showRawParameters} + /> ); })} diff --git a/src/components/CodeEditor.jsx b/src/components/CodeEditor.jsx old mode 100644 new mode 100755 diff --git a/src/components/DarkModeToggle.jsx b/src/components/DarkModeToggle.jsx new file mode 100755 index 00000000..b2b3ca0d --- /dev/null +++ b/src/components/DarkModeToggle.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useTheme } from '../contexts/ThemeContext'; + +function DarkModeToggle() { + const { isDarkMode, toggleDarkMode } = useTheme(); + + return ( +
Toggle dark mode + + {isDarkMode ? ( + + + + ) : ( + + + + )} + + + ); +} + +export default DarkModeToggle; \ No newline at end of file diff --git a/src/components/FileTree.jsx b/src/components/FileTree.jsx old mode 100644 new mode 100755 diff --git a/src/components/ImageViewer.jsx b/src/components/ImageViewer.jsx old mode 100644 new mode 100755 diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx old mode 100644 new mode 100755 index ab2099eb..9f5b5818 --- a/src/components/MainContent.jsx +++ b/src/components/MainContent.jsx @@ -34,7 +34,10 @@ function MainContent({ onSessionActive, // Mark session as active when user sends message onSessionInactive, // Mark session as inactive when conversation completes/aborts onReplaceTemporarySession, // Replace temporary session ID with real session ID from WebSocket - onNavigateToSession // Navigate to a specific session (for Claude CLI session duplication workaround) + onNavigateToSession, // Navigate to a specific session (for Claude CLI session duplication workaround) + onShowSettings, // Show tools settings panel + autoExpandTools, // Auto-expand tool accordions + showRawParameters // Show raw parameters in tool accordions }) { const [editingFile, setEditingFile] = useState(null); @@ -239,6 +242,10 @@ function MainContent({ onSessionInactive={onSessionInactive} onReplaceTemporarySession={onReplaceTemporarySession} onNavigateToSession={onNavigateToSession} + onShowSettings={onShowSettings} + autoExpandTools={autoExpandTools} + showRawParameters={showRawParameters} + />
diff --git a/src/components/MobileNav.jsx b/src/components/MobileNav.jsx old mode 100644 new mode 100755 diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx new file mode 100755 index 00000000..f663acde --- /dev/null +++ b/src/components/QuickSettingsPanel.jsx @@ -0,0 +1,179 @@ +import React, { useState, useEffect } from 'react'; +import { + ChevronLeft, + ChevronRight, + Maximize2, + Eye, + EyeOff, + Zap, + Layout, + Terminal, + Code2, + Settings2, + Moon, + Sun +} from 'lucide-react'; +import DarkModeToggle from './DarkModeToggle'; +import { useTheme } from '../contexts/ThemeContext'; + +const QuickSettingsPanel = ({ + isOpen, + onToggle, + autoExpandTools, + onAutoExpandChange, + showRawParameters, + onShowRawParametersChange, + isMobile +}) => { + const [localIsOpen, setLocalIsOpen] = useState(isOpen); + const { isDarkMode } = useTheme(); + + useEffect(() => { + setLocalIsOpen(isOpen); + }, [isOpen]); + + const handleToggle = () => { + const newState = !localIsOpen; + setLocalIsOpen(newState); + onToggle(newState); + }; + + return ( + <> + {/* Pull Tab */} +
+ +
+ + {/* Panel */} +
+
+ {/* Header */} +
+

+ + Quick Settings +

+
+ + {/* Settings Content */} +
+ {/* Appearance Settings */} +
+

Appearance

+ +
+ + {isDarkMode ? : } + Dark Mode + + +
+
+ + {/* Tool Display Settings */} +
+

Tool Display

+ + + + + + +
+ + {/* Performance Settings */} +
+

Performance

+ + + + +
+ + {/* View Options */} +
+

View Options

+ + + + +
+
+ + {/* Footer */} +
+ +
+
+
+ + {/* Backdrop for mobile */} + {isMobile && localIsOpen && ( +
+ )} + + ); +}; + +export default QuickSettingsPanel; \ No newline at end of file diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx old mode 100644 new mode 100755 diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx old mode 100644 new mode 100755 diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx old mode 100644 new mode 100755 index 8bfbda7c..9ddd559e --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -10,43 +10,43 @@ const TodoList = ({ todos, isResult = false }) => { const getStatusIcon = (status) => { switch (status) { case 'completed': - return ; + return ; case 'in_progress': - return ; + return ; case 'pending': default: - return ; + return ; } }; const getStatusColor = (status) => { switch (status) { case 'completed': - return 'bg-green-100 text-green-800 border-green-200'; + return 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border-green-200 dark:border-green-800'; case 'in_progress': - return 'bg-blue-100 text-blue-800 border-blue-200'; + return 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200 border-blue-200 dark:border-blue-800'; case 'pending': default: - return 'bg-gray-100 text-gray-600 border-gray-200'; + return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'; } }; const getPriorityColor = (priority) => { switch (priority) { case 'high': - return 'bg-red-100 text-red-700 border-red-200'; + return 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800'; case 'medium': - return 'bg-yellow-100 text-yellow-700 border-yellow-200'; + return 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800'; case 'low': default: - return 'bg-gray-100 text-gray-600 border-gray-200'; + return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700'; } }; return (
{isResult && ( -
+
Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
)} @@ -54,7 +54,7 @@ const TodoList = ({ todos, isResult = false }) => { {todos.map((todo) => (
{getStatusIcon(todo.status)} @@ -62,7 +62,7 @@ const TodoList = ({ todos, isResult = false }) => {
-

+

{todo.content}

diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx old mode 100644 new mode 100755 index e945a7bf..da4aae2d --- a/src/components/ToolsSettings.jsx +++ b/src/components/ToolsSettings.jsx @@ -3,9 +3,11 @@ import { Button } from './ui/button'; import { Input } from './ui/input'; import { ScrollArea } from './ui/scroll-area'; import { Badge } from './ui/badge'; -import { X, Plus, Settings, Shield, AlertTriangle } from 'lucide-react'; +import { X, Plus, Settings, Shield, AlertTriangle, Moon, Sun } from 'lucide-react'; +import { useTheme } from '../contexts/ThemeContext'; function ToolsSettings({ isOpen, onClose }) { + const { isDarkMode, toggleDarkMode } = useTheme(); const [allowedTools, setAllowedTools] = useState([]); const [disallowedTools, setDisallowedTools] = useState([]); const [newAllowedTool, setNewAllowedTool] = useState(''); @@ -140,6 +142,48 @@ function ToolsSettings({ isOpen, onClose }) {
+ {/* Theme Settings */} +
+
+ {isDarkMode ? : } +

+ Appearance +

+
+
+
+
+
+ Dark Mode +
+
+ Toggle between light and dark themes +
+
+ +
+
+
+ {/* Skip Permissions */}
diff --git a/src/components/ui/badge.jsx b/src/components/ui/badge.jsx old mode 100644 new mode 100755 diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx old mode 100644 new mode 100755 diff --git a/src/components/ui/input.jsx b/src/components/ui/input.jsx old mode 100644 new mode 100755 diff --git a/src/components/ui/scroll-area.jsx b/src/components/ui/scroll-area.jsx old mode 100644 new mode 100755 diff --git a/src/contexts/ThemeContext.jsx b/src/contexts/ThemeContext.jsx new file mode 100755 index 00000000..f2a37647 --- /dev/null +++ b/src/contexts/ThemeContext.jsx @@ -0,0 +1,72 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +const ThemeContext = createContext(); + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; + +export const ThemeProvider = ({ children }) => { + // Check for saved theme preference or default to system preference + const [isDarkMode, setIsDarkMode] = useState(() => { + // Check localStorage first + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + return savedTheme === 'dark'; + } + + // Check system preference + if (window.matchMedia) { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + return false; + }); + + // Update document class and localStorage when theme changes + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add('dark'); + localStorage.setItem('theme', 'dark'); + } else { + document.documentElement.classList.remove('dark'); + localStorage.setItem('theme', 'light'); + } + }, [isDarkMode]); + + // Listen for system theme changes + useEffect(() => { + if (!window.matchMedia) return; + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = (e) => { + // Only update if user hasn't manually set a preference + const savedTheme = localStorage.getItem('theme'); + if (!savedTheme) { + setIsDarkMode(e.matches); + } + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + const toggleDarkMode = () => { + setIsDarkMode(prev => !prev); + }; + + const value = { + isDarkMode, + toggleDarkMode, + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css old mode 100644 new mode 100755 index 689130c8..29a9858f --- a/src/index.css +++ b/src/index.css @@ -5,47 +5,47 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 240 10% 3.9%; + --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; + --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 5.9% 10%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; --radius: 0.5rem; } .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 217.2 91.2% 8%; + --card-foreground: 210 40% 98%; + --popover: 217.2 91.2% 8%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; } } diff --git a/src/lib/utils.js b/src/lib/utils.js old mode 100644 new mode 100755 diff --git a/src/main.jsx b/src/main.jsx old mode 100644 new mode 100755 diff --git a/src/utils/websocket.js b/src/utils/websocket.js old mode 100644 new mode 100755 diff --git a/tailwind.config.js b/tailwind.config.js old mode 100644 new mode 100755 diff --git a/vite.config.js b/vite.config.js old mode 100644 new mode 100755 From 3b0a612c9cbbbac42ca9cee51c06204951ad67d2 Mon Sep 17 00:00:00 2001 From: Simos Date: Fri, 4 Jul 2025 11:30:14 +0200 Subject: [PATCH 2/3] Update package dependencies, add Git API routes, and implement audio transcription functionality. Introduce new components for Git management, enhance chat interface with microphone support, and improve UI elements for better user experience. --- package-lock.json | 426 +++++++------- public/icons/claude-ai-icon.svg | 1 + server/index.js | 156 ++++++ server/routes/git.js | 388 +++++++++++++ src/App.jsx | 57 +- src/components/ChatInterface.jsx | 673 ++++++++++++++++++++-- src/components/ClaudeLogo.jsx | 11 + src/components/ClaudeStatus.jsx | 107 ++++ src/components/GitPanel.jsx | 777 ++++++++++++++++++++++++++ src/components/MainContent.jsx | 84 ++- src/components/MicButton.jsx | 217 +++++++ src/components/MobileNav.jsx | 9 +- src/components/QuickSettingsPanel.jsx | 167 ++++-- src/components/Shell.jsx | 16 +- src/components/Sidebar.jsx | 134 ++++- src/hooks/useAudioRecorder.js | 109 ++++ src/index.css | 485 ++++++++++++++++ src/utils/whisper.js | 38 ++ 18 files changed, 3495 insertions(+), 360 deletions(-) create mode 100755 public/icons/claude-ai-icon.svg create mode 100755 server/routes/git.js create mode 100755 src/components/ClaudeLogo.jsx create mode 100755 src/components/ClaudeStatus.jsx create mode 100755 src/components/GitPanel.jsx create mode 100755 src/components/MicButton.jsx create mode 100755 src/hooks/useAudioRecorder.js create mode 100755 src/utils/whisper.js diff --git a/package-lock.json b/package-lock.json index 703503ea..171b3be0 100755 --- a/package-lock.json +++ b/package-lock.json @@ -75,9 +75,9 @@ } }, "node_modules/@anthropic-ai/claude-code": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.24.tgz", - "integrity": "sha512-4S6ly2297ngNlto7IFZeEicS9u0yRDhocOzndWFovGBb+iUoEPKdZa/rhVk/tcyCADL6S+mMkiGQOlqFDrN3JQ==", + "version": "1.0.43", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.43.tgz", + "integrity": "sha512-VnuRK4s/R9ZRTkwH4gUjsp4SiBQXq7Y0B47OtgeXIZYVQYkhTW8m+E0IisFzXXFIyTQrE0SodGCpvgLhAYzGCg==", "hasInstallScript": true, "bin": { "claude": "cli.js" @@ -109,30 +109,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", - "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -148,15 +148,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -179,6 +179,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -259,12 +268,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -326,27 +335,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -421,9 +430,9 @@ } }, "node_modules/@codemirror/lang-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", - "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" @@ -456,9 +465,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz", - "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", + "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -497,9 +506,9 @@ } }, "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", - "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -508,9 +517,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.37.2", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz", - "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "version": "6.38.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.0.tgz", + "integrity": "sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==", "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1298,16 +1307,12 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1318,23 +1323,15 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1475,15 +1472,15 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", - "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.1.tgz", + "integrity": "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==", "cpu": [ "arm" ], @@ -1494,9 +1491,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", - "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.1.tgz", + "integrity": "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==", "cpu": [ "arm64" ], @@ -1507,9 +1504,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", - "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.1.tgz", + "integrity": "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==", "cpu": [ "arm64" ], @@ -1520,9 +1517,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", - "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.1.tgz", + "integrity": "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==", "cpu": [ "x64" ], @@ -1533,9 +1530,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", - "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.1.tgz", + "integrity": "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==", "cpu": [ "arm64" ], @@ -1546,9 +1543,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", - "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.1.tgz", + "integrity": "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==", "cpu": [ "x64" ], @@ -1559,9 +1556,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", - "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.1.tgz", + "integrity": "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==", "cpu": [ "arm" ], @@ -1572,9 +1569,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", - "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.1.tgz", + "integrity": "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==", "cpu": [ "arm" ], @@ -1585,9 +1582,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", - "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.1.tgz", + "integrity": "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==", "cpu": [ "arm64" ], @@ -1598,9 +1595,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", - "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.1.tgz", + "integrity": "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==", "cpu": [ "arm64" ], @@ -1611,9 +1608,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", - "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.1.tgz", + "integrity": "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==", "cpu": [ "loong64" ], @@ -1624,9 +1621,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", - "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.1.tgz", + "integrity": "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==", "cpu": [ "ppc64" ], @@ -1637,9 +1634,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", - "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.1.tgz", + "integrity": "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==", "cpu": [ "riscv64" ], @@ -1650,9 +1647,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", - "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.1.tgz", + "integrity": "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==", "cpu": [ "riscv64" ], @@ -1663,9 +1660,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", - "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.1.tgz", + "integrity": "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==", "cpu": [ "s390x" ], @@ -1676,9 +1673,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", - "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.1.tgz", + "integrity": "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==", "cpu": [ "x64" ], @@ -1689,9 +1686,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", - "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.1.tgz", + "integrity": "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==", "cpu": [ "x64" ], @@ -1702,9 +1699,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", - "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.1.tgz", + "integrity": "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==", "cpu": [ "arm64" ], @@ -1715,9 +1712,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", - "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.1.tgz", + "integrity": "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==", "cpu": [ "ia32" ], @@ -1728,9 +1725,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", - "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz", + "integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==", "cpu": [ "x64" ], @@ -1754,18 +1751,6 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, - "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1816,9 +1801,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", @@ -1878,9 +1863,9 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.23.13", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.13.tgz", - "integrity": "sha512-U1CnDFpq6ydNqrRDS5Bdnvgso8ezwwbrmKvmAD3hmoVyRDsDU6HTtmcV+w0rZ3kElUCkKI5lY0DMvTTQ4+L3RQ==", + "version": "4.23.14", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.14.tgz", + "integrity": "sha512-lCseubZqjN9bFwHJdQlZEKEo2yO1tCiMMVL0gu3ZXwhqMdfnd6ky/fUCYbn8aJkW+cXKVwjEVhpKjOphNiHoNw==", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -1904,15 +1889,15 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.23.13", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.13.tgz", - "integrity": "sha512-y65ULzxOAfpxrA/8epoAOeCfmJXu9z0P62BbGOkITJTtU7WI59KfPbbwj35npSsMAkAmDE841qZo2I8jst/THg==", + "version": "4.23.14", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.14.tgz", + "integrity": "sha512-/CmlSh8LGUEZCxg/f78MEkEMehKnVklqJvJlL10AXXrO/2xOyPqHb8SK10GhwOqd0kHhHgVYp4+6oK5S+UIEuQ==", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.23.13", + "@uiw/codemirror-extensions-basic-setup": "4.23.14", "codemirror": "^6.0.0" }, "funding": { @@ -1934,15 +1919,15 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" }, "node_modules/@vitejs/plugin-react": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.2.tgz", - "integrity": "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", "dev": true, "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.11", + "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -2179,9 +2164,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", - "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "dev": true, "funding": [ { @@ -2198,8 +2183,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001718", - "electron-to-chromium": "^1.5.160", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -2254,9 +2239,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001723", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", - "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==", + "version": "1.0.30001726", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz", + "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==", "dev": true, "funding": [ { @@ -2452,9 +2437,9 @@ } }, "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -2756,9 +2741,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.167", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz", - "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==", + "version": "1.5.179", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", + "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", "dev": true }, "node_modules/emoji-regex": { @@ -3156,15 +3141,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4442,9 +4418,9 @@ } }, "node_modules/postcss": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", - "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -4560,7 +4536,7 @@ "postcss": "^8.2.14" } }, - "node_modules/postcss-selector-parser": { + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", @@ -4572,6 +4548,18 @@ "node": ">=4" } }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -4830,12 +4818,12 @@ } }, "node_modules/rollup": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", - "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", + "version": "4.44.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", + "integrity": "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==", "dev": true, "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -4845,26 +4833,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.43.0", - "@rollup/rollup-android-arm64": "4.43.0", - "@rollup/rollup-darwin-arm64": "4.43.0", - "@rollup/rollup-darwin-x64": "4.43.0", - "@rollup/rollup-freebsd-arm64": "4.43.0", - "@rollup/rollup-freebsd-x64": "4.43.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", - "@rollup/rollup-linux-arm-musleabihf": "4.43.0", - "@rollup/rollup-linux-arm64-gnu": "4.43.0", - "@rollup/rollup-linux-arm64-musl": "4.43.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-gnu": "4.43.0", - "@rollup/rollup-linux-riscv64-musl": "4.43.0", - "@rollup/rollup-linux-s390x-gnu": "4.43.0", - "@rollup/rollup-linux-x64-gnu": "4.43.0", - "@rollup/rollup-linux-x64-musl": "4.43.0", - "@rollup/rollup-win32-arm64-msvc": "4.43.0", - "@rollup/rollup-win32-ia32-msvc": "4.43.0", - "@rollup/rollup-win32-x64-msvc": "4.43.0", + "@rollup/rollup-android-arm-eabi": "4.44.1", + "@rollup/rollup-android-arm64": "4.44.1", + "@rollup/rollup-darwin-arm64": "4.44.1", + "@rollup/rollup-darwin-x64": "4.44.1", + "@rollup/rollup-freebsd-arm64": "4.44.1", + "@rollup/rollup-freebsd-x64": "4.44.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", + "@rollup/rollup-linux-arm-musleabihf": "4.44.1", + "@rollup/rollup-linux-arm64-gnu": "4.44.1", + "@rollup/rollup-linux-arm64-musl": "4.44.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-gnu": "4.44.1", + "@rollup/rollup-linux-riscv64-musl": "4.44.1", + "@rollup/rollup-linux-s390x-gnu": "4.44.1", + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1", + "@rollup/rollup-win32-arm64-msvc": "4.44.1", + "@rollup/rollup-win32-ia32-msvc": "4.44.1", + "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" } }, @@ -5663,6 +5651,18 @@ "node": ">= 6" } }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/tailwindcss/node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6107,9 +6107,9 @@ } }, "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "engines": { "node": ">=10.0.0" }, diff --git a/public/icons/claude-ai-icon.svg b/public/icons/claude-ai-icon.svg new file mode 100755 index 00000000..853a243c --- /dev/null +++ b/public/icons/claude-ai-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/index.js b/server/index.js index a3a308d3..e87ad76e 100755 --- a/server/index.js +++ b/server/index.js @@ -28,8 +28,11 @@ const fs = require('fs').promises; const { spawn } = require('child_process'); const os = require('os'); const pty = require('node-pty'); +const fetch = require('node-fetch'); + const { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually } = require('./projects'); const { spawnClaude, abortClaudeSession } = require('./claude-cli'); +const gitRoutes = require('./routes/git'); // File system watcher for projects folder let projectsWatcher = null; @@ -144,6 +147,9 @@ app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, '../dist'))); +// Git API Routes +app.use('/api/git', gitRoutes); + // API Routes app.get('/api/config', (req, res) => { // Always use the server's actual IP and port for WebSocket connections @@ -651,6 +657,156 @@ function handleShellConnection(ws) { console.error('❌ Shell WebSocket error:', error); }); } +// Audio transcription endpoint +app.post('/api/transcribe', async (req, res) => { + try { + const multer = require('multer'); + const upload = multer({ storage: multer.memoryStorage() }); + + // Handle multipart form data + upload.single('audio')(req, res, async (err) => { + if (err) { + return res.status(400).json({ error: 'Failed to process audio file' }); + } + + if (!req.file) { + return res.status(400).json({ error: 'No audio file provided' }); + } + + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + return res.status(500).json({ error: 'OpenAI API key not configured. Please set OPENAI_API_KEY in server environment.' }); + } + + try { + // Create form data for OpenAI + const FormData = require('form-data'); + const formData = new FormData(); + formData.append('file', req.file.buffer, { + filename: req.file.originalname, + contentType: req.file.mimetype + }); + formData.append('model', 'whisper-1'); + formData.append('response_format', 'json'); + formData.append('language', 'en'); + + // Make request to OpenAI + const fetch = require('node-fetch'); + const response = await fetch('https://api.openai.com/v1/audio/transcriptions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + ...formData.getHeaders() + }, + body: formData + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error?.message || `Whisper API error: ${response.status}`); + } + + const data = await response.json(); + let transcribedText = data.text || ''; + + // Check if enhancement mode is enabled + const mode = req.body.mode || 'default'; + + // If no transcribed text, return empty + if (!transcribedText) { + return res.json({ text: '' }); + } + + // If default mode, return transcribed text without enhancement + if (mode === 'default') { + return res.json({ text: transcribedText }); + } + + // Handle different enhancement modes + try { + const OpenAI = require('openai'); + const openai = new OpenAI({ apiKey }); + + let prompt, systemMessage, temperature = 0.7, maxTokens = 800; + + switch (mode) { + case 'prompt': + systemMessage = 'You are an expert prompt engineer who creates clear, detailed, and effective prompts.'; + prompt = `You are an expert prompt engineer. Transform the following rough instruction into a clear, detailed, and context-aware AI prompt. + +Your enhanced prompt should: +1. Be specific and unambiguous +2. Include relevant context and constraints +3. Specify the desired output format +4. Use clear, actionable language +5. Include examples where helpful +6. Consider edge cases and potential ambiguities + +Transform this rough instruction into a well-crafted prompt: +"${transcribedText}" + +Enhanced prompt:`; + break; + + case 'vibe': + case 'instructions': + case 'architect': + systemMessage = 'You are a helpful assistant that formats ideas into clear, actionable instructions for AI agents.'; + temperature = 0.5; // Lower temperature for more controlled output + prompt = `Transform the following idea into clear, well-structured instructions that an AI agent can easily understand and execute. + +IMPORTANT RULES: +- Format as clear, step-by-step instructions +- Add reasonable implementation details based on common patterns +- Only include details directly related to what was asked +- Do NOT add features or functionality not mentioned +- Keep the original intent and scope intact +- Use clear, actionable language an agent can follow + +Transform this idea into agent-friendly instructions: +"${transcribedText}" + +Agent instructions:`; + break; + + default: + // No enhancement needed + break; + } + + // Only make GPT call if we have a prompt + if (prompt) { + const completion = await openai.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { role: 'system', content: systemMessage }, + { role: 'user', content: prompt } + ], + temperature: temperature, + max_tokens: maxTokens + }); + + transcribedText = completion.choices[0].message.content || transcribedText; + } + + } catch (gptError) { + console.error('GPT processing error:', gptError); + // Fall back to original transcription if GPT fails + } + + res.json({ text: transcribedText }); + + } catch (error) { + console.error('Transcription error:', error); + res.status(500).json({ error: error.message }); + } + }); + } catch (error) { + console.error('Endpoint error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + // Serve React app for all other routes app.get('*', (req, res) => { diff --git a/server/routes/git.js b/server/routes/git.js new file mode 100755 index 00000000..ebe97947 --- /dev/null +++ b/server/routes/git.js @@ -0,0 +1,388 @@ +const express = require('express'); +const { exec } = require('child_process'); +const { promisify } = require('util'); +const path = require('path'); +const fs = require('fs').promises; + +const router = express.Router(); +const execAsync = promisify(exec); + +// Helper function to get the actual project path from the encoded project name +function getActualProjectPath(projectName) { + // Claude stores projects with dashes instead of slashes + // Convert "-Users-dmieloch-Dev-experiments-claudecodeui" to "/Users/dmieloch/Dev/experiments/claudecodeui" + return projectName.replace(/-/g, '/'); +} + +// Get git status for a project +router.get('/status', async (req, res) => { + const { project } = req.query; + + if (!project) { + return res.status(400).json({ error: 'Project name is required' }); + } + + try { + const projectPath = getActualProjectPath(project); + console.log('Git status for project:', project, '-> path:', projectPath); + + // Check if directory exists + try { + await fs.access(projectPath); + } catch { + console.error('Project path not found:', projectPath); + return res.json({ error: 'Project not found' }); + } + + // Check if it's a git repository + try { + await execAsync('git rev-parse --git-dir', { cwd: projectPath }); + } catch { + console.error('Not a git repository:', projectPath); + return res.json({ error: 'Not a git repository' }); + } + + // Get current branch + const { stdout: branch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath }); + + // Get git status + const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectPath }); + + const modified = []; + const added = []; + const deleted = []; + const untracked = []; + + statusOutput.split('\n').forEach(line => { + if (!line.trim()) return; + + const status = line.substring(0, 2); + const file = line.substring(3); + + if (status === 'M ' || status === ' M' || status === 'MM') { + modified.push(file); + } else if (status === 'A ' || status === 'AM') { + added.push(file); + } else if (status === 'D ' || status === ' D') { + deleted.push(file); + } else if (status === '??') { + untracked.push(file); + } + }); + + res.json({ + branch: branch.trim(), + modified, + added, + deleted, + untracked + }); + } catch (error) { + console.error('Git status error:', error); + res.json({ error: error.message }); + } +}); + +// Get diff for a specific file +router.get('/diff', async (req, res) => { + const { project, file } = req.query; + + if (!project || !file) { + return res.status(400).json({ error: 'Project name and file path are required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Check if file is untracked + const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath }); + const isUntracked = statusOutput.startsWith('??'); + + let diff; + if (isUntracked) { + // For untracked files, show the entire file content as additions + const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8'); + const lines = fileContent.split('\n'); + diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` + + lines.map(line => `+${line}`).join('\n'); + } else { + // Get diff for tracked files + const { stdout } = await execAsync(`git diff HEAD -- "${file}"`, { cwd: projectPath }); + diff = stdout || ''; + + // If no unstaged changes, check for staged changes + if (!diff) { + const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath }); + diff = stagedDiff; + } + } + + res.json({ diff }); + } catch (error) { + console.error('Git diff error:', error); + res.json({ error: error.message }); + } +}); + +// Commit changes +router.post('/commit', async (req, res) => { + const { project, message, files } = req.body; + + if (!project || !message || !files || files.length === 0) { + return res.status(400).json({ error: 'Project name, commit message, and files are required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Stage selected files + for (const file of files) { + await execAsync(`git add "${file}"`, { cwd: projectPath }); + } + + // Commit with message + const { stdout } = await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: projectPath }); + + res.json({ success: true, output: stdout }); + } catch (error) { + console.error('Git commit error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Get list of branches +router.get('/branches', async (req, res) => { + const { project } = req.query; + + if (!project) { + return res.status(400).json({ error: 'Project name is required' }); + } + + try { + const projectPath = getActualProjectPath(project); + console.log('Git branches for project:', project, '-> path:', projectPath); + + // Get all branches + const { stdout } = await execAsync('git branch -a', { cwd: projectPath }); + + // Parse branches + const branches = stdout + .split('\n') + .map(branch => branch.trim()) + .filter(branch => branch && !branch.includes('->')) // Remove empty lines and HEAD pointer + .map(branch => { + // Remove asterisk from current branch + if (branch.startsWith('* ')) { + return branch.substring(2); + } + // Remove remotes/ prefix + if (branch.startsWith('remotes/origin/')) { + return branch.substring(15); + } + return branch; + }) + .filter((branch, index, self) => self.indexOf(branch) === index); // Remove duplicates + + res.json({ branches }); + } catch (error) { + console.error('Git branches error:', error); + res.json({ error: error.message }); + } +}); + +// Checkout branch +router.post('/checkout', async (req, res) => { + const { project, branch } = req.body; + + if (!project || !branch) { + return res.status(400).json({ error: 'Project name and branch are required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Checkout the branch + const { stdout } = await execAsync(`git checkout "${branch}"`, { cwd: projectPath }); + + res.json({ success: true, output: stdout }); + } catch (error) { + console.error('Git checkout error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Create new branch +router.post('/create-branch', async (req, res) => { + const { project, branch } = req.body; + + if (!project || !branch) { + return res.status(400).json({ error: 'Project name and branch name are required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Create and checkout new branch + const { stdout } = await execAsync(`git checkout -b "${branch}"`, { cwd: projectPath }); + + res.json({ success: true, output: stdout }); + } catch (error) { + console.error('Git create branch error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Get recent commits +router.get('/commits', async (req, res) => { + const { project, limit = 10 } = req.query; + + if (!project) { + return res.status(400).json({ error: 'Project name is required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Get commit log with stats + const { stdout } = await execAsync( + `git log --pretty=format:'%H|%an|%ae|%ad|%s' --date=relative -n ${limit}`, + { cwd: projectPath } + ); + + const commits = stdout + .split('\n') + .filter(line => line.trim()) + .map(line => { + const [hash, author, email, date, ...messageParts] = line.split('|'); + return { + hash, + author, + email, + date, + message: messageParts.join('|') + }; + }); + + // Get stats for each commit + for (const commit of commits) { + try { + const { stdout: stats } = await execAsync( + `git show --stat --format='' ${commit.hash}`, + { cwd: projectPath } + ); + commit.stats = stats.trim().split('\n').pop(); // Get the summary line + } catch (error) { + commit.stats = ''; + } + } + + res.json({ commits }); + } catch (error) { + console.error('Git commits error:', error); + res.json({ error: error.message }); + } +}); + +// Get diff for a specific commit +router.get('/commit-diff', async (req, res) => { + const { project, commit } = req.query; + + if (!project || !commit) { + return res.status(400).json({ error: 'Project name and commit hash are required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Get diff for the commit + const { stdout } = await execAsync( + `git show ${commit}`, + { cwd: projectPath } + ); + + res.json({ diff: stdout }); + } catch (error) { + console.error('Git commit diff error:', error); + res.json({ error: error.message }); + } +}); + +// Generate commit message based on staged changes +router.post('/generate-commit-message', async (req, res) => { + const { project, files } = req.body; + + if (!project || !files || files.length === 0) { + return res.status(400).json({ error: 'Project name and files are required' }); + } + + try { + const projectPath = getActualProjectPath(project); + + // Get diff for selected files + let combinedDiff = ''; + for (const file of files) { + try { + const { stdout } = await execAsync( + `git diff HEAD -- "${file}"`, + { cwd: projectPath } + ); + if (stdout) { + combinedDiff += `\n--- ${file} ---\n${stdout}`; + } + } catch (error) { + console.error(`Error getting diff for ${file}:`, error); + } + } + + // Use AI to generate commit message (simple implementation) + // In a real implementation, you might want to use GPT or Claude API + const message = generateSimpleCommitMessage(files, combinedDiff); + + res.json({ message }); + } catch (error) { + console.error('Generate commit message error:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Simple commit message generator (can be replaced with AI) +function generateSimpleCommitMessage(files, diff) { + const fileCount = files.length; + const isMultipleFiles = fileCount > 1; + + // Analyze the diff to determine the type of change + const additions = (diff.match(/^\+[^+]/gm) || []).length; + const deletions = (diff.match(/^-[^-]/gm) || []).length; + + // Determine the primary action + let action = 'Update'; + if (additions > 0 && deletions === 0) { + action = 'Add'; + } else if (deletions > 0 && additions === 0) { + action = 'Remove'; + } else if (additions > deletions * 2) { + action = 'Enhance'; + } else if (deletions > additions * 2) { + action = 'Refactor'; + } + + // Generate message based on files + if (isMultipleFiles) { + const components = new Set(files.map(f => { + const parts = f.split('/'); + return parts[parts.length - 2] || parts[0]; + })); + + if (components.size === 1) { + return `${action} ${[...components][0]} component`; + } else { + return `${action} multiple components`; + } + } else { + const fileName = files[0].split('/').pop(); + const componentName = fileName.replace(/\.(jsx?|tsx?|css|scss)$/, ''); + return `${action} ${componentName}`; + } +} + +module.exports = router; \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index dcfac641..c515fe33 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -53,6 +53,10 @@ function AppContent() { const saved = localStorage.getItem('showRawParameters'); return saved !== null ? JSON.parse(saved) : false; }); + const [autoScrollToBottom, setAutoScrollToBottom] = useState(() => { + const saved = localStorage.getItem('autoScrollToBottom'); + return saved !== null ? JSON.parse(saved) : true; + }); // Session Protection System: Track sessions with active conversations to prevent // automatic project updates from interrupting ongoing chats. When a user sends // a message, the session is marked as "active" and project updates are paused @@ -217,13 +221,18 @@ function AppContent() { // Handle URL-based session loading useEffect(() => { if (sessionId && projects.length > 0) { + // Only switch tabs on initial load, not on every project update + const shouldSwitchTab = !selectedSession || selectedSession.id !== sessionId; // Find the session across all projects for (const project of projects) { const session = project.sessions?.find(s => s.id === sessionId); if (session) { setSelectedProject(project); setSelectedSession(session); - setActiveTab('chat'); + // Only switch to chat tab if we're loading a different session + if (shouldSwitchTab) { + setActiveTab('chat'); + } return; } } @@ -245,7 +254,11 @@ function AppContent() { const handleSessionSelect = (session) => { setSelectedSession(session); - setActiveTab('chat'); + // Only switch to chat tab when user explicitly selects a session + // This prevents tab switching during automatic updates + if (activeTab !== 'git' && activeTab !== 'preview') { + setActiveTab('chat'); + } if (isMobile) { setSidebarOpen(false); } @@ -482,23 +495,29 @@ function AppContent() { isInputFocused={isInputFocused} /> )} - - {/* Quick Settings Panel */} - { - setAutoExpandTools(value); - localStorage.setItem('autoExpandTools', JSON.stringify(value)); - }} - showRawParameters={showRawParameters} - onShowRawParametersChange={(value) => { - setShowRawParameters(value); - localStorage.setItem('showRawParameters', JSON.stringify(value)); - }} - isMobile={isMobile} - /> + {/* Quick Settings Panel - Only show on chat tab */} + {activeTab === 'chat' && ( + { + setAutoExpandTools(value); + localStorage.setItem('autoExpandTools', JSON.stringify(value)); + }} + showRawParameters={showRawParameters} + onShowRawParametersChange={(value) => { + setShowRawParameters(value); + localStorage.setItem('showRawParameters', JSON.stringify(value)); + }} + autoScrollToBottom={autoScrollToBottom} + onAutoScrollChange={(value) => { + setAutoScrollToBottom(value); + localStorage.setItem('autoScrollToBottom', JSON.stringify(value)); + }} + isMobile={isMobile} + /> + )} {/* Tools Settings Modal */} { const isGrouped = prevMessage && prevMessage.type === message.type && prevMessage.type === 'assistant' && !prevMessage.isToolUse && !message.isToolUse; - const messageRef = React.useRef(null); const [isExpanded, setIsExpanded] = React.useState(false); - React.useEffect(() => { if (!autoExpandTools || !messageRef.current || !message.isToolUse) return; @@ -89,8 +91,8 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile !
) : ( -
- C +
+
)}
@@ -195,7 +197,8 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
))}
-
{showRawParameters && ( +
+ {showRawParameters && (
View raw parameters @@ -205,7 +208,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)} -
); @@ -225,6 +227,101 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile ); })()} {message.toolInput && message.toolName !== 'Edit' && (() => { + // Debug log to see what we're dealing with + console.log('Tool display - name:', message.toolName, 'input type:', typeof message.toolInput); + + // Special handling for Write tool + if (message.toolName === 'Write') { + console.log('Write tool detected, toolInput:', message.toolInput); + try { + let input; + // Handle both JSON string and already parsed object + if (typeof message.toolInput === 'string') { + input = JSON.parse(message.toolInput); + } else { + input = message.toolInput; + } + + console.log('Parsed Write input:', input); + + if (input.file_path && input.content !== undefined) { + return ( +
+ + + + + 📄 Creating new file: + + +
+
+
+ + + New File + +
+
+ {createDiff('', input.content).map((diffLine, i) => ( +
+ + {diffLine.type === 'removed' ? '-' : '+'} + + + {diffLine.content} + +
+ ))} +
+
+ {showRawParameters && ( +
+ + View raw parameters + +
+                                    {message.toolInput}
+                                  
+
+ )} +
+
+ ); + } + } catch (e) { + // Fall back to regular display + } + } + // Special handling for TodoWrite tool if (message.toolName === 'TodoWrite') { try { @@ -251,7 +348,100 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)} - +
+ ); + } + } catch (e) { + // Fall back to regular display + } + } + + // Special handling for Bash tool + if (message.toolName === 'Bash') { + try { + const input = JSON.parse(message.toolInput); + return ( +
+ + + + + Running command + +
+
+
+ + + + Terminal +
+
+ $ {input.command} +
+
+ {input.description && ( +
+ {input.description} +
+ )} + {showRawParameters && ( +
+ + View raw parameters + +
+                                  {message.toolInput}
+                                
+
+ )} +
+
+ ); + } catch (e) { + // Fall back to regular display + } + } + + // Special handling for Read tool + if (message.toolName === 'Read') { + try { + const input = JSON.parse(message.toolInput); + if (input.file_path) { + // Extract filename + const filename = input.file_path.split('/').pop(); + const pathParts = input.file_path.split('/'); + const directoryPath = pathParts.slice(0, -1).join('/'); + + // Simple heuristic to show only relevant path parts + // Show the last 2-3 directory parts before the filename + const relevantParts = pathParts.slice(-4, -1); // Get up to 3 directories before filename + const relativePath = relevantParts.length > 0 ? relevantParts.join('/') + '/' : ''; + + return ( +
+ + + + + + + + {relativePath} + {filename} + + {showRawParameters && ( +
+
+ + View raw parameters + +
+                                    {message.toolInput}
+                                  
+
+
+ )}
); } @@ -345,6 +535,106 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile // Fall through to regular handling } } + + // Special handling for interactive prompts + if (content.includes('Do you want to proceed?') && message.toolName === 'Bash') { + const lines = content.split('\n'); + const promptIndex = lines.findIndex(line => line.includes('Do you want to proceed?')); + const beforePrompt = lines.slice(0, promptIndex).join('\n'); + const promptLines = lines.slice(promptIndex); + + // Extract the question and options + const questionLine = promptLines.find(line => line.includes('Do you want to proceed?')) || ''; + const options = []; + + // Parse numbered options (1. Yes, 2. No, etc.) + promptLines.forEach(line => { + const optionMatch = line.match(/^\s*(\d+)\.\s+(.+)$/); + if (optionMatch) { + options.push({ + number: optionMatch[1], + text: optionMatch[2].trim() + }); + } + }); + + // Find which option was selected (usually indicated by "> 1" or similar) + const selectedMatch = content.match(/>\s*(\d+)/); + const selectedOption = selectedMatch ? selectedMatch[1] : null; + + return ( +
+ {beforePrompt && ( +
+
{beforePrompt}
+
+ )} +
+
+
+ + + +
+
+

+ Interactive Prompt +

+

+ {questionLine} +

+ + {/* Option buttons */} +
+ {options.map((option) => ( + + ))} +
+ + {selectedOption && ( +
+

+ ✓ Claude selected option {selectedOption} +

+

+ In the CLI, you would select this option interactively using arrow keys or by typing the number. +

+
+ )} +
+
+
+
+ ); + } const fileEditMatch = content.match(/The file (.+?) has been updated\./); if (fileEditMatch) { @@ -363,6 +653,43 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile ); } + // Handle Write tool output for file creation + const fileCreateMatch = content.match(/(?:The file|File) (.+?) has been (?:created|written)(?: successfully)?\.?/); + if (fileCreateMatch) { + return ( +
+
+ File created successfully +
+ +
+ ); + } + + // Special handling for Write tool - hide content if it's just the file content + if (message.toolName === 'Write' && !message.toolResult.isError) { + // For Write tool, the diff is already shown in the tool input section + // So we just show a success message here + return ( +
+
+ + + + File written successfully +
+

+ The file content is displayed in the diff view above +

+
+ ); + } + if (content.includes('cat -n') && content.includes('→')) { return (
@@ -407,17 +734,100 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile )} + ) : message.isInteractivePrompt ? ( + // Special handling for interactive prompts +
+
+
+ + + +
+
+

+ Interactive Prompt +

+ {(() => { + const lines = message.content.split('\n').filter(line => line.trim()); + const questionLine = lines.find(line => line.includes('?')) || lines[0] || ''; + const options = []; + + // Parse the menu options + lines.forEach(line => { + // Match lines like "❯ 1. Yes" or " 2. No" + const optionMatch = line.match(/[❯\s]*(\d+)\.\s+(.+)/); + if (optionMatch) { + const isSelected = line.includes('❯'); + options.push({ + number: optionMatch[1], + text: optionMatch[2].trim(), + isSelected + }); + } + }); + + return ( + <> +

+ {questionLine} +

+ + {/* Option buttons */} +
+ {options.map((option) => ( + + ))} +
+ +
+

+ ⏳ Waiting for your response in the CLI +

+

+ Please select an option in your terminal where Claude is running. +

+
+ + ); + })()} +
+
+
) : (
{message.type === 'assistant' ? ( -
+
{ return inline ? ( - + {children} - + ) : (
@@ -454,7 +864,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)} -
+
{new Date(message.timestamp).toLocaleTimeString()}
@@ -472,7 +882,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile // - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID // // This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations. -function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters }) { +function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom }) { const [input, setInput] = useState(() => { if (typeof window !== 'undefined' && selectedProject) { return localStorage.getItem(`draft_input_${selectedProject.name}`) || ''; @@ -504,6 +914,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const [atSymbolPosition, setAtSymbolPosition] = useState(-1); const [canAbortSession, setCanAbortSession] = useState(false); const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); + const scrollPositionRef = useRef({ height: 0, top: 0 }); + const [showCommandMenu, setShowCommandMenu] = useState(false); + const [slashCommands, setSlashCommands] = useState([]); + const [filteredCommands, setFilteredCommands] = useState([]); + const [isTextareaExpanded, setIsTextareaExpanded] = useState(false); + const [selectedCommandIndex, setSelectedCommandIndex] = useState(-1); + const [slashPosition, setSlashPosition] = useState(-1); + const [claudeStatus, setClaudeStatus] = useState(null); // Memoized diff calculation to prevent recalculating on every render @@ -718,8 +1136,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const messages = await loadSessionMessages(selectedProject.name, selectedSession.id); setSessionMessages(messages); // convertedMessages will be automatically updated via useMemo - // Scroll to bottom after loading session messages - setTimeout(() => scrollToBottom(), 200); + // Scroll to bottom after loading session messages if auto-scroll is enabled + if (autoScrollToBottom) { + setTimeout(() => scrollToBottom(), 200); + } } else { // Reset the flag after handling system session change setIsSystemSessionChange(false); @@ -920,7 +1340,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess timestamp: new Date() }]); break; - + case 'claude-interactive-prompt': + // Handle interactive prompts from CLI + setChatMessages(prev => [...prev, { + type: 'assistant', + content: latestMessage.data, + timestamp: new Date(), + isInteractivePrompt: true + }]); + break; + case 'claude-error': setChatMessages(prev => [...prev, { type: 'error', @@ -932,6 +1361,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'claude-complete': setIsLoading(false); setCanAbortSession(false); + setClaudeStatus(null); + // Session Protection: Mark session as inactive to re-enable automatic project updates // Conversation is complete, safe to allow project updates again @@ -952,6 +1383,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'session-aborted': setIsLoading(false); setCanAbortSession(false); + setClaudeStatus(null); // Session Protection: Mark session as inactive when aborted // User or system aborted the conversation, re-enable project updates @@ -960,13 +1392,52 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } setChatMessages(prev => [...prev, { - type: 'error', - content: latestMessage.success ? - 'Session aborted successfully' : - 'Failed to abort session - it may have already completed', + type: 'assistant', + content: 'Session interrupted by user.', timestamp: new Date() }]); break; + + case 'claude-status': + // Handle Claude working status messages + console.log('🔔 Received claude-status message:', latestMessage); + const statusData = latestMessage.data; + if (statusData) { + // Parse the status message to extract relevant information + let statusInfo = { + text: 'Working...', + tokens: 0, + can_interrupt: true + }; + + // Check for different status message formats + if (statusData.message) { + statusInfo.text = statusData.message; + } else if (statusData.status) { + statusInfo.text = statusData.status; + } else if (typeof statusData === 'string') { + statusInfo.text = statusData; + } + + // Extract token count + if (statusData.tokens) { + statusInfo.tokens = statusData.tokens; + } else if (statusData.token_count) { + statusInfo.tokens = statusData.token_count; + } + + // Check if can interrupt + if (statusData.can_interrupt !== undefined) { + statusInfo.can_interrupt = statusData.can_interrupt; + } + + console.log('📊 Setting claude status:', statusInfo); + setClaudeStatus(statusInfo); + setIsLoading(true); + setCanAbortSession(statusInfo.can_interrupt); + } + break; + } } }, [messages]); @@ -1057,21 +1528,48 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess return chatMessages.slice(-maxMessages); }, [chatMessages]); + // Capture scroll position before render when auto-scroll is disabled useEffect(() => { - // Only auto-scroll to bottom when new messages arrive if user hasn't scrolled up + if (!autoScrollToBottom && scrollContainerRef.current) { + const container = scrollContainerRef.current; + scrollPositionRef.current = { + height: container.scrollHeight, + top: container.scrollTop + }; + } + }); + + useEffect(() => { + // Only auto-scroll to bottom when new messages arrive if: + // 1. Auto-scroll is enabled in settings + // 2. User hasn't manually scrolled up if (scrollContainerRef.current && chatMessages.length > 0) { - if (!isUserScrolledUp) { - setTimeout(() => scrollToBottom(), 0); + if (autoScrollToBottom) { + if (!isUserScrolledUp) { + setTimeout(() => scrollToBottom(), 0); + } + } else { + // When auto-scroll is disabled, preserve the visual position + const container = scrollContainerRef.current; + const prevHeight = scrollPositionRef.current.height; + const prevTop = scrollPositionRef.current.top; + const newHeight = container.scrollHeight; + const heightDiff = newHeight - prevHeight; + + // If content was added above the current view, adjust scroll position + if (heightDiff > 0 && prevTop > 0) { + container.scrollTop = prevTop + heightDiff; + } } } - }, [chatMessages.length, isUserScrolledUp, scrollToBottom]); + }, [chatMessages.length, isUserScrolledUp, scrollToBottom, autoScrollToBottom]); // Scroll to bottom when component mounts with existing messages useEffect(() => { - if (scrollContainerRef.current && chatMessages.length > 0) { + if (scrollContainerRef.current && chatMessages.length > 0 && autoScrollToBottom) { setTimeout(() => scrollToBottom(), 100); // Small delay to ensure rendering } - }, [scrollToBottom]); + }, [scrollToBottom, autoScrollToBottom]); // Add scroll event listener to detect user scrolling useEffect(() => { @@ -1087,9 +1585,36 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (textareaRef.current) { textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; + + // Check if initially expanded + const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight); + const isExpanded = textareaRef.current.scrollHeight > lineHeight * 2; + setIsTextareaExpanded(isExpanded); } }, []); // Only run once on mount + const handleTranscript = useCallback((text) => { + if (text.trim()) { + setInput(prevInput => { + const newInput = prevInput.trim() ? `${prevInput} ${text}` : text; + + // Update textarea height after setting new content + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto'; + textareaRef.current.style.height = textareaRef.current.scrollHeight + 'px'; + + // Check if expanded after transcript + const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight); + const isExpanded = textareaRef.current.scrollHeight > lineHeight * 2; + setIsTextareaExpanded(isExpanded); + } + }, 0); + + return newInput; + }); + } + }, []); const handleSubmit = (e) => { e.preventDefault(); @@ -1104,6 +1629,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setChatMessages(prev => [...prev, userMessage]); setIsLoading(true); setCanAbortSession(true); + // Set a default status when starting + setClaudeStatus({ + text: 'Processing', + tokens: 0, + can_interrupt: true + }); // Always scroll to bottom when user sends a message (they're actively participating) setTimeout(() => scrollToBottom(), 0); @@ -1151,6 +1682,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }); setInput(''); + setIsTextareaExpanded(false); // Clear the saved draft since message was sent if (selectedProject) { localStorage.removeItem(`draft_input_${selectedProject.name}`); @@ -1245,7 +1777,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setCanAbortSession(false); }; - // Abort functionality is not yet implemented at the backend + const handleAbortSession = () => { + if (currentSessionId && canAbortSession) { + sendMessage({ + type: 'abort-session', + sessionId: currentSessionId + }); + } + }; // Don't render if no project is selected if (!selectedProject) { @@ -1281,11 +1820,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
) : chatMessages.length === 0 ? ( -
-

Start a conversation with Claude

-

- Ask questions about your code, request changes, or get help with development tasks -

+
+
+

Start a conversation with Claude

+

+ Ask questions about your code, request changes, or get help with development tasks +

+
) : ( <> @@ -1312,7 +1853,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess onShowSettings={onShowSettings} autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} - /> ); })} @@ -1347,7 +1887,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess {isUserScrolledUp && chatMessages.length > 0 && (