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.

This commit is contained in:
Simos
2025-07-03 23:15:36 +02:00
parent 01481f9114
commit 845d5346eb
64 changed files with 562 additions and 100 deletions

17
.claude/settings.local.json Executable file
View File

@@ -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": []
}
}

0
.gitignore vendored Normal file → Executable file
View File

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
index.html Normal file → Executable file
View File

0
package-lock.json generated Normal file → Executable file
View File

0
package.json Normal file → Executable file
View File

0
postcss.config.js Normal file → Executable file
View File

0
public/convert-icons.md Normal file → Executable file
View File

0
public/favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 281 B

0
public/favicon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 504 B

After

Width:  |  Height:  |  Size: 504 B

0
public/generate-icons.js Normal file → Executable file
View File

0
public/icons/generate-icons.md Normal file → Executable file
View File

0
public/icons/icon-128x128.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-128x128.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-144x144.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-144x144.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-152x152.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-152x152.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-192x192.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-192x192.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-384x384.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-384x384.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-512x512.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-512x512.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-72x72.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-72x72.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-96x96.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/icons/icon-96x96.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/icons/icon-template.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

0
public/logo.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 422 B

0
public/manifest.json Normal file → Executable file
View File

0
public/screenshots/desktop-main.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

0
public/screenshots/mobile-chat.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

0
public/screenshots/tools-modal.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 295 KiB

0
public/sw.js Normal file → Executable file
View File

0
server/claude-cli.js Normal file → Executable file
View File

0
server/claude-cli.js.backup.1750077611635 Normal file → Executable file
View File

0
server/index.js Normal file → Executable file
View File

32
server/projects.js Normal file → Executable file
View File

@@ -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: []
};

47
src/App.jsx Normal file → Executable file
View File

@@ -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}
/>
</div>
@@ -466,6 +482,23 @@ function AppContent() {
isInputFocused={isInputFocused}
/>
)}
{/* Quick Settings Panel */}
<QuickSettingsPanel
isOpen={showQuickSettings}
onToggle={setShowQuickSettings}
autoExpandTools={autoExpandTools}
onAutoExpandChange={(value) => {
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 */}
<ToolsSettings
@@ -479,12 +512,14 @@ function AppContent() {
// Root App component with router
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<AppContent />} />
<Route path="/session/:sessionId" element={<AppContent />} />
</Routes>
</Router>
<ThemeProvider>
<Router>
<Routes>
<Route path="/" element={<AppContent />} />
<Route path="/session/:sessionId" element={<AppContent />} />
</Routes>
</Router>
</ThemeProvider>
);
}

135
src/components/ChatInterface.jsx Normal file → Executable file
View File

@@ -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 (
<div
ref={messageRef}
className={`chat-message ${message.type} ${isGrouped ? 'grouped' : ''} ${message.type === 'user' ? 'flex justify-end px-3 sm:px-0' : 'px-3 sm:px-0'}`}
>
{message.type === 'user' ? (
@@ -71,26 +103,43 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
{message.isToolUse ? (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-2 sm:p-3 mb-2">
<div className="flex items-center gap-2 mb-2">
<div className="w-5 h-5 bg-blue-600 rounded flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-5 h-5 bg-blue-600 rounded flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<span className="font-medium text-blue-900 dark:text-blue-100">
Using {message.toolName}
</span>
<span className="text-xs text-blue-600 dark:text-blue-400 font-mono">
{message.toolId}
</span>
</div>
<span className="font-medium text-blue-900 dark:text-blue-100">
Using {message.toolName}
</span>
<span className="text-xs text-blue-600 dark:text-blue-400 font-mono">
{message.toolId}
</span>
{onShowSettings && (
<button
onClick={(e) => {
e.stopPropagation();
onShowSettings();
}}
className="p-1 rounded hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
title="Tool Settings"
>
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
)}
</div>
{message.toolInput && message.toolName === 'Edit' && (() => {
try {
const input = JSON.parse(message.toolInput);
if (input.file_path && input.old_string && input.new_string) {
return (
<details className="mt-2">
<details className="mt-2" open={autoExpandTools}>
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
@@ -146,15 +195,17 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
</div>
))}
</div>
</div>
<details className="mt-2">
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
View raw parameters
</summary>
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
{message.toolInput}
</pre>
</details>
</div> {showRawParameters && (
<details className="mt-2" open={autoExpandTools}>
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
View raw parameters
</summary>
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
{message.toolInput}
</pre>
</details>
)}
</div>
</details>
);
@@ -163,7 +214,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
// Fall back to raw display if parsing fails
}
return (
<details className="mt-2">
<details className="mt-2" open={autoExpandTools}>
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200">
View input parameters
</summary>
@@ -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 (
<details className="mt-2">
<details className="mt-2" open={autoExpandTools}>
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
@@ -189,15 +240,18 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
</summary>
<div className="mt-3">
<TodoList todos={input.todos} />
<details className="mt-3">
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
View raw parameters
</summary>
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded overflow-x-auto text-blue-900 dark:text-blue-100">
{message.toolInput}
</pre>
</details>
{showRawParameters && (
<details className="mt-3" open={autoExpandTools}>
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
View raw parameters
</summary>
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded overflow-x-auto text-blue-900 dark:text-blue-100">
{message.toolInput}
</pre>
</details>
)}
</div>
</details>
);
}
@@ -208,7 +262,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
// Regular tool input display for other tools
return (
<details className="mt-2">
<details className="mt-2" open={autoExpandTools}>
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-2">
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
@@ -311,7 +365,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
if (content.includes('cat -n') && content.includes('→')) {
return (
<details>
<details open={autoExpandTools}>
<summary className="text-sm text-green-700 dark:text-green-300 cursor-pointer hover:text-green-800 dark:hover:text-green-200 mb-2 flex items-center gap-2">
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
@@ -329,7 +383,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
if (content.length > 300) {
return (
<details>
<details open={autoExpandTools}>
<summary className="text-sm text-green-700 dark:text-green-300 cursor-pointer hover:text-green-800 dark:hover:text-green-200 mb-2 flex items-center gap-2">
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
@@ -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}
/>
);
})}

0
src/components/CodeEditor.jsx Normal file → Executable file
View File

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { useTheme } from '../contexts/ThemeContext';
function DarkModeToggle() {
const { isDarkMode, toggleDarkMode } = useTheme();
return (
<button
onClick={toggleDarkMode}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={isDarkMode}
aria-label="Toggle dark mode"
>
<span className="sr-only">Toggle dark mode</span>
<span
className={`${
isDarkMode ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
>
{isDarkMode ? (
<svg className="w-3.5 h-3.5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="w-3.5 h-3.5 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</span>
</button>
);
}
export default DarkModeToggle;

0
src/components/FileTree.jsx Normal file → Executable file
View File

0
src/components/ImageViewer.jsx Normal file → Executable file
View File

9
src/components/MainContent.jsx Normal file → Executable file
View File

@@ -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}
/>
</div>
<div className={`h-full overflow-hidden ${activeTab === 'files' ? 'block' : 'hidden'}`}>

0
src/components/MobileNav.jsx Normal file → Executable file
View File

View File

@@ -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 */}
<div
className={`fixed ${isMobile ? 'bottom-44' : 'top-1/2 -translate-y-1/2'} ${
localIsOpen ? 'right-64' : 'right-0'
} z-40 transition-all duration-300 ease-in-out`}
>
<button
onClick={handleToggle}
className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-l-md p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors shadow-lg"
aria-label={localIsOpen ? 'Close settings panel' : 'Open settings panel'}
>
{localIsOpen ? (
<ChevronRight className="h-5 w-5 text-gray-600 dark:text-gray-400" />
) : (
<ChevronLeft className="h-5 w-5 text-gray-600 dark:text-gray-400" />
)}
</button>
</div>
{/* Panel */}
<div
className={`fixed top-0 right-0 h-full w-64 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl transform transition-transform duration-300 ease-in-out z-30 ${
localIsOpen ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className="h-full flex flex-col">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Settings2 className="h-5 w-5 text-gray-600 dark:text-gray-400" />
Quick Settings
</h3>
</div>
{/* Settings Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-6 bg-white dark:bg-gray-800">
{/* Appearance Settings */}
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Appearance</h4>
<div className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
{isDarkMode ? <Moon className="h-4 w-4 text-gray-600 dark:text-gray-400" /> : <Sun className="h-4 w-4 text-gray-600 dark:text-gray-400" />}
Dark Mode
</span>
<DarkModeToggle />
</div>
</div>
{/* Tool Display Settings */}
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Tool Display</h4>
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
<Maximize2 className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Auto-expand tools
</span>
<input
type="checkbox"
checked={autoExpandTools}
onChange={(e) => onAutoExpandChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
/>
</label>
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
<Eye className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Show raw parameters
</span>
<input
type="checkbox"
checked={showRawParameters}
onChange={(e) => onShowRawParametersChange(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
/>
</label>
<button disabled className="w-full flex items-center gap-2 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-left text-gray-500 dark:text-gray-400 transition-colors opacity-60 cursor-not-allowed">
<Code2 className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Syntax highlighting
</button>
</div>
{/* Performance Settings */}
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Performance</h4>
<button disabled className="w-full flex items-center gap-2 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-left text-gray-500 dark:text-gray-400 transition-colors opacity-60 cursor-not-allowed">
<Zap className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Stream optimizations
</button>
<button disabled className="w-full flex items-center gap-2 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-left text-gray-500 dark:text-gray-400 transition-colors opacity-60 cursor-not-allowed">
<Layout className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Compact mode
</button>
</div>
{/* View Options */}
<div className="space-y-2">
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">View Options</h4>
<button disabled className="w-full flex items-center gap-2 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-left text-gray-500 dark:text-gray-400 transition-colors opacity-60 cursor-not-allowed">
<Terminal className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Terminal theme
</button>
<button disabled className="w-full flex items-center gap-2 p-3 rounded-lg bg-gray-50 dark:bg-gray-700/50 hover:bg-gray-100 dark:hover:bg-gray-700 text-sm text-left text-gray-500 dark:text-gray-400 transition-colors opacity-60 cursor-not-allowed">
<EyeOff className="h-4 w-4 text-gray-600 dark:text-gray-400" />
Hide timestamps
</button>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
<button disabled className="w-full py-2 px-4 bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400 rounded-md transition-colors text-sm cursor-not-allowed opacity-60">
Advanced Settings
</button>
</div>
</div>
</div>
{/* Backdrop for mobile */}
{isMobile && localIsOpen && (
<div
className="fixed inset-0 bg-black/20 z-20"
onClick={handleToggle}
/>
)}
</>
);
};
export default QuickSettingsPanel;

0
src/components/Shell.jsx Normal file → Executable file
View File

0
src/components/Sidebar.jsx Normal file → Executable file
View File

24
src/components/TodoList.jsx Normal file → Executable file
View File

@@ -10,43 +10,43 @@ const TodoList = ({ todos, isResult = false }) => {
const getStatusIcon = (status) => {
switch (status) {
case 'completed':
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
return <CheckCircle2 className="w-4 h-4 text-green-500 dark:text-green-400" />;
case 'in_progress':
return <Clock className="w-4 h-4 text-blue-500" />;
return <Clock className="w-4 h-4 text-blue-500 dark:text-blue-400" />;
case 'pending':
default:
return <Circle className="w-4 h-4 text-gray-400" />;
return <Circle className="w-4 h-4 text-gray-400 dark:text-gray-500" />;
}
};
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 (
<div className="space-y-3">
{isResult && (
<div className="text-sm font-medium text-gray-700 mb-3">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Todo List ({todos.length} {todos.length === 1 ? 'item' : 'items'})
</div>
)}
@@ -54,7 +54,7 @@ const TodoList = ({ todos, isResult = false }) => {
{todos.map((todo) => (
<div
key={todo.id}
className="flex items-start gap-3 p-3 bg-white border rounded-lg shadow-sm hover:shadow-md transition-shadow"
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md dark:shadow-gray-900/50 transition-shadow"
>
<div className="flex-shrink-0 mt-0.5">
{getStatusIcon(todo.status)}
@@ -62,7 +62,7 @@ const TodoList = ({ todos, isResult = false }) => {
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-2">
<p className={`text-sm font-medium ${todo.status === 'completed' ? 'line-through text-gray-500' : 'text-gray-900'}`}>
<p className={`text-sm font-medium ${todo.status === 'completed' ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-gray-100'}`}>
{todo.content}
</p>

46
src/components/ToolsSettings.jsx Normal file → Executable file
View File

@@ -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 }) {
<div className="flex-1 overflow-y-auto">
<div className="p-4 md:p-6 space-y-6 md:space-y-8 pb-safe-area-inset-bottom">
{/* Theme Settings */}
<div className="space-y-4">
<div className="flex items-center gap-3">
{isDarkMode ? <Moon className="w-5 h-5 text-blue-500" /> : <Sun className="w-5 h-5 text-yellow-500" />}
<h3 className="text-lg font-medium text-foreground">
Appearance
</h3>
</div>
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-foreground">
Dark Mode
</div>
<div className="text-sm text-muted-foreground">
Toggle between light and dark themes
</div>
</div>
<button
onClick={toggleDarkMode}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
role="switch"
aria-checked={isDarkMode}
aria-label="Toggle dark mode"
>
<span className="sr-only">Toggle dark mode</span>
<span
className={`${
isDarkMode ? 'translate-x-7' : 'translate-x-1'
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
>
{isDarkMode ? (
<Moon className="w-3.5 h-3.5 text-gray-700" />
) : (
<Sun className="w-3.5 h-3.5 text-yellow-500" />
)}
</span>
</button>
</div>
</div>
</div>
{/* Skip Permissions */}
<div className="space-y-4">
<div className="flex items-center gap-3">

0
src/components/ui/badge.jsx Normal file → Executable file
View File

0
src/components/ui/button.jsx Normal file → Executable file
View File

0
src/components/ui/input.jsx Normal file → Executable file
View File

0
src/components/ui/scroll-area.jsx Normal file → Executable file
View File

72
src/contexts/ThemeContext.jsx Executable file
View File

@@ -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 (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};

66
src/index.css Normal file → Executable file
View File

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

0
src/lib/utils.js Normal file → Executable file
View File

0
src/main.jsx Normal file → Executable file
View File

0
src/utils/websocket.js Normal file → Executable file
View File

0
tailwind.config.js Normal file → Executable file
View File

0
vite.config.js Normal file → Executable file
View File