52 Commits

Author SHA1 Message Date
viper151
7d0fd141ff Merge pull request #203 from SyedaAnshrahGillani/fix-accessibility-issues
feat: Improve accessibility and refactor settings management
2025-10-01 22:21:35 +02:00
SyedaAnshrahGillani
e5a05d9865 fix: Address coderabbitai feedback 2025-10-02 01:12:52 +05:00
SyedaAnshrahGillani
2d6c3b5755 refactor: Create useLocalStorage hook to reduce code duplication 2025-10-01 17:16:09 +05:00
SyedaAnshrahGillani
2a5d27ffc0 fix(accessibility): Use buttons for modal backdrops 2025-10-01 17:04:35 +05:00
viper151
3c9a4cab82 Fix: Prevent CLI option injection in --print argument
Fix: Prevent CLI option injection in --print argument
2025-09-23 11:31:51 +02:00
viper151
ce9ab0cd16 Merge branch 'main' into fix-injection 2025-09-23 11:29:43 +02:00
viper151
66fad9a6a2 Update .env.example 2025-09-23 11:29:15 +02:00
viper151
0fcf906ff0 Merge pull request #197 from johnhenry/johnhenry/env-claude-path
Feat: Use environment variable for Claude path
2025-09-23 11:26:44 +02:00
viper151
7e1f2940d3 Merge branch 'main' into johnhenry/env-claude-path 2025-09-23 11:25:56 +02:00
simos
533d589132 Release 1.8.10 2025-09-23 10:54:35 +02:00
simos
d1f310161f fixes on changelog prettify 2025-09-23 10:54:00 +02:00
simos
b853e8cda1 Release 1.8.9 2025-09-23 10:51:45 +02:00
simos
1610de1f22 prettifying change logs 2025-09-23 10:51:18 +02:00
viper151
3f743d8210 Merge branch 'main' into johnhenry/env-claude-path 2025-09-23 10:48:59 +02:00
simos
cbb18fb010 Release 1.8.8 2025-09-23 10:38:53 +02:00
simos
d8d754274a modified: .release-it.json 2025-09-23 10:37:20 +02:00
simos
eb12aef641 modified: .release-it.json 2025-09-23 10:37:02 +02:00
simos
5e574dbdec modified: package.json 2025-09-23 10:36:04 +02:00
simos
af9e9eec02 modified: .release-it.json 2025-09-23 10:35:38 +02:00
simos
8c3ee770c3 fixing release-it 2025-09-23 10:35:14 +02:00
simos
58108c083c modified: package-lock.json 2025-09-23 10:33:08 +02:00
simos
af0ad6b4b6 fixes on npm 2025-09-23 10:28:24 +02:00
simos
36d9f47e29 Fixing release it 2025-09-23 10:24:55 +02:00
simos
1f25f1e79b Adding changelog to release-it 2025-09-23 10:22:16 +02:00
simos
d7ed1de1cb Release 1.8.6 2025-09-23 10:17:02 +02:00
simos
9be54233d0 fixs for npmjs package 2025-09-23 10:14:26 +02:00
simos
c8bcad71e7 Release 1.8.5 2025-09-23 02:16:12 +02:00
simos
a3f504aed2 adding executable 2025-09-23 02:15:32 +02:00
simos
f766ac1517 fixes 2025-09-23 02:13:36 +02:00
simos
680d8f6fb1 Release 1.8.4 2025-09-23 02:09:10 +02:00
simos
1820f3bfa8 Release 1.8.3 2025-09-23 01:40:58 +02:00
simos
376e055443 Adding files to npm package 2025-09-23 01:40:21 +02:00
simos
f4becdc218 Release 1.8.2 2025-09-23 01:24:23 +02:00
simos
70b421e5a8 changes to package.json to support npm releases 2025-09-23 01:23:43 +02:00
John Henry
d74a99ef24 Merge branch 'main' into johnhenry/env-claude-path 2025-09-22 10:53:48 -07:00
simos
771f7e4d7b Fix: UI issues 2025-09-17 09:10:27 +00:00
simos
cab13a1534 Fix: UI fixes for mobile 2025-09-17 08:43:22 +00:00
simos
1eba594418 Feat: Improve the chat interface by grouping messages from AI 2025-09-17 08:04:43 +00:00
simos
ed5374a1bd Feat: Make the login modal full screen for better visibility 2025-09-17 08:04:21 +00:00
simos
573a04d2e5 Feat: Add search bar on file tree 2025-09-17 08:00:24 +00:00
John Henry
133af82935 Merge branch 'main' into johnhenry/env-claude-path 2025-09-15 14:30:59 -07:00
simos
79981693f3 Fix : mobile issues and git diff in the git panel 2025-09-15 20:57:49 +00:00
simos
fb1117a999 Fix:
Fixed mobile zoom on input
2025-09-15 20:31:33 +00:00
simos
40b8737732 Merge branch 'main' of https://github.com/siteboon/claudecodeui 2025-09-15 15:57:45 +00:00
simos
15b95c4d08 Fixing maximum depth of directories 2025-09-15 15:56:33 +00:00
viper151
3ff1db0331 Merge pull request #188 from takumi3488/fix/ios-pwa-status-bar-overlap
fix: iOS PWA status bar overlap issue on mobile devices
2025-09-15 17:41:48 +02:00
viper151
dab0068d8f Merge branch 'main' into fix/ios-pwa-status-bar-overlap 2025-09-15 17:40:44 +02:00
viper151
3daf21c3d1 Merge branch 'main' into johnhenry/env-claude-path 2025-09-15 17:40:05 +02:00
John
f52ca8e702 Use environment variable for Claude path 2025-09-14 09:35:58 -07:00
Takumi Mori
11b2ff588a Merge branch 'main' into fix/ios-pwa-status-bar-overlap 2025-09-14 02:41:00 +09:00
Takumi Mori
975e4b04a6 fix: iOS PWA status bar overlap issue on mobile devices
- Add PWA detection logic to App.jsx
- Apply dynamic padding to header and sidebar in PWA mode
- Update viewport meta tag for better iOS compatibility
- Change status bar style to black-translucent for PWA
- Add CSS variables for safe area insets with fallback support

This ensures menu button and sidebar content are properly visible
below the iOS status bar when installed as a PWA.
2025-09-01 00:55:13 +09:00
Terrasse
d82a004224 fix prompt injection bug 2025-08-27 18:23:00 +08:00
19 changed files with 2730 additions and 155 deletions

View File

@@ -9,4 +9,7 @@
#API server
PORT=3001
#Frontend port
VITE_PORT=5173
VITE_PORT=5173
# Uncomment the following line if you have a custom claude cli path other than the default "claude"
# CLAUDE_CLI_PATH=claude

21
.release-it.json Normal file
View File

@@ -0,0 +1,21 @@
{
"git": {
"commitMessage": "Release ${version}",
"tagName": "v${version}",
"changelog": "git log --pretty=format:\"* %s (%h)\" ${from}...${to}"
},
"npm": {
"publish": true
},
"github": {
"release": true,
"releaseName": "Claude Code UI v${version}",
"releaseNotes": {
"commit": "* ${commit.subject} (${sha}){ - thanks @${author.login}!}",
"excludeMatches": ["viper151"]
}
},
"hooks": {
"before:init": ["npm run build"]
}
}

View File

@@ -59,7 +59,17 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or
- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured
### Installation
### One-click Operation (Recommended)
No installation required, direct operation:
```bash
npx @siteboon/claude-code-ui
```
Your default browser will automatically open the Claude Code UI interface.
### Local Development Installation
1. **Clone the repository:**
```bash

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Claude Code UI</title>
<!-- PWA Manifest -->
@@ -12,7 +12,7 @@
<!-- iOS Safari PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Claude UI" />
<!-- iOS Safari Icons -->

2285
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,40 @@
{
"name": "claude-code-ui",
"version": "1.8.1",
"name": "@siteboon/claude-code-ui",
"version": "1.8.10",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",
"bin": {
"claude-code-ui": "server/index.js"
},
"files": [
"server/",
"dist/",
"README.md"
],
"homepage": "https://claudecodeui.siteboon.ai",
"repository": {
"type": "git",
"url": "git+https://github.com/siteboon/claudecodeui.git"
},
"bugs": {
"url": "https://github.com/siteboon/claudecodeui/issues"
},
"scripts": {
"dev": "concurrently --kill-others \"npm run server\" \"npm run client\"",
"server": "node server/index.js",
"client": "vite --host",
"build": "vite build",
"preview": "vite preview",
"start": "npm run build && npm run server"
"start": "npm run build && npm run server",
"release": "release-it"
},
"keywords": [
"claude",
"claude coode",
"ai",
"code",
"anthropic",
"ui",
"assistant"
"mobile"
],
"author": "Claude Code UI Contributors",
"license": "MIT",
@@ -63,10 +80,12 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.6.0",
"auto-changelog": "^2.5.0",
"autoprefixer": "^10.4.16",
"concurrently": "^8.2.2",
"node-gyp": "^10.0.0",
"postcss": "^8.4.32",
"release-it": "^19.0.5",
"sharp": "^0.34.2",
"tailwindcss": "^3.4.0",
"vite": "^7.0.4"

View File

@@ -25,15 +25,6 @@ async function spawnClaude(command, options = {}, ws) {
// Build Claude CLI command - start with print/resume flags first
const args = [];
// Add print flag with command if we have a command
if (command && command.trim()) {
// Separate arguments for better cross-platform compatibility
// This prevents issues with spaces and quotes on Windows
args.push('--print');
args.push(command);
}
// Use cwd (actual project directory) instead of projectPath (Claude's metadata directory)
const workingDir = cwd || process.cwd();
@@ -225,6 +216,17 @@ async function spawnClaude(command, options = {}, ws) {
console.log('📝 Skip permissions disabled due to plan mode');
}
}
// Add print flag with command if we have a command
if (command && command.trim()) {
// Separate arguments for better cross-platform compatibility
// This prevents issues with spaces and quotes on Windows
args.push('--print');
// Use `--` so user input is always treated as text, not options
args.push('--');
args.push(command);
}
console.log('Spawning Claude CLI:', 'claude', args.map(arg => {
const cleanArg = arg.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
@@ -235,7 +237,11 @@ async function spawnClaude(command, options = {}, ws) {
console.log('🔍 Full command args:', JSON.stringify(args, null, 2));
console.log('🔍 Final Claude command will be: claude ' + args.join(' '));
const claudeProcess = spawnFunction('claude', args, {
// Use Claude CLI from environment variable or default to 'claude'
const claudePath = process.env.CLAUDE_CLI_PATH || 'claude';
console.log('🔍 Using Claude CLI path:', claudePath);
const claudeProcess = spawnFunction(claudePath, args, {
cwd: workingDir,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env } // Inherit all environment variables

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env node
// Load environment variables from .env file
import fs from 'fs';
import path from 'path';
@@ -506,7 +507,7 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
}
const files = await getFileTree(actualPath, 3, 0, true);
const files = await getFileTree(actualPath, 10, 0, true);
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
res.json(files);
} catch (error) {

View File

@@ -899,22 +899,16 @@ async function addProjectManually(projectPath, displayName = null) {
// Generate project name (encode path for use as directory name)
const projectName = absolutePath.replace(/\//g, '-');
// Check if project already exists in config or as a folder
// Check if project already exists in config
const config = await loadProjectConfig();
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
try {
await fs.access(projectDir);
throw new Error(`Project already exists for path: ${absolutePath}`);
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
if (config[projectName]) {
throw new Error(`Project already configured for path: ${absolutePath}`);
}
// Allow adding projects even if the directory exists - this enables tracking
// existing Claude Code or Cursor projects in the UI
// Add to config as manually added project
config[projectName] = {

View File

@@ -33,6 +33,7 @@ import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext';
import ProtectedRoute from './components/ProtectedRoute';
import { useVersionCheck } from './hooks/useVersionCheck';
import useLocalStorage from './hooks/useLocalStorage';
import { api, authenticatedFetch } from './utils/api';
@@ -54,22 +55,10 @@ function AppContent() {
const [isInputFocused, setIsInputFocused] = useState(false);
const [showSettings, setShowSettings] = 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;
});
const [autoScrollToBottom, setAutoScrollToBottom] = useState(() => {
const saved = localStorage.getItem('autoScrollToBottom');
return saved !== null ? JSON.parse(saved) : true;
});
const [sendByCtrlEnter, setSendByCtrlEnter] = useState(() => {
const saved = localStorage.getItem('sendByCtrlEnter');
return saved !== null ? JSON.parse(saved) : false;
});
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', 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
@@ -77,6 +66,37 @@ function AppContent() {
const [activeSessions, setActiveSessions] = useState(new Set()); // Track sessions with active conversations
const { ws, sendMessage, messages } = useWebSocketContext();
// Detect if running as PWA
const [isPWA, setIsPWA] = useState(false);
useEffect(() => {
// Check if running in standalone mode (PWA)
const checkPWA = () => {
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone ||
document.referrer.includes('android-app://');
setIsPWA(isStandalone);
// Add class to html and body for CSS targeting
if (isStandalone) {
document.documentElement.classList.add('pwa-mode');
document.body.classList.add('pwa-mode');
} else {
document.documentElement.classList.remove('pwa-mode');
document.body.classList.remove('pwa-mode');
}
};
checkPWA();
// Listen for changes
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
return () => {
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
};
}, []);
useEffect(() => {
const checkMobile = () => {
@@ -460,9 +480,10 @@ function AppContent() {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
<button
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={() => setShowVersionModal(false)}
aria-label="Close version upgrade modal"
/>
{/* Modal */}
@@ -561,6 +582,8 @@ function AppContent() {
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
isPWA={isPWA}
isMobile={isMobile}
/>
</div>
</div>
@@ -571,7 +594,7 @@ function AppContent() {
<div className={`fixed inset-0 z-50 flex transition-all duration-150 ease-out ${
sidebarOpen ? 'opacity-100 visible' : 'opacity-0 invisible'
}`}>
<div
<button
className="fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-150 ease-out"
onClick={(e) => {
e.stopPropagation();
@@ -582,6 +605,7 @@ function AppContent() {
e.stopPropagation();
setSidebarOpen(false);
}}
aria-label="Close sidebar"
/>
<div
className={`relative w-[85vw] max-w-sm sm:w-80 bg-card border-r border-border transform transition-transform duration-150 ease-out ${
@@ -607,13 +631,15 @@ function AppContent() {
latestVersion={latestVersion}
currentVersion={currentVersion}
onShowVersionModal={() => setShowVersionModal(true)}
isPWA={isPWA}
isMobile={isMobile}
/>
</div>
</div>
)}
{/* Main Content Area - Flexible */}
<div className="flex-1 flex flex-col min-w-0">
<div className={`flex-1 flex flex-col min-w-0 ${isMobile && !isInputFocused ? 'pb-16' : ''}`}>
<MainContent
selectedProject={selectedProject}
selectedSession={selectedSession}
@@ -623,6 +649,7 @@ function AppContent() {
sendMessage={sendMessage}
messages={messages}
isMobile={isMobile}
isPWA={isPWA}
onMenuClick={() => setSidebarOpen(true)}
isLoading={isLoadingProjects}
onInputFocusChange={setIsInputFocused}
@@ -652,25 +679,13 @@ function AppContent() {
isOpen={showQuickSettings}
onToggle={setShowQuickSettings}
autoExpandTools={autoExpandTools}
onAutoExpandChange={(value) => {
setAutoExpandTools(value);
localStorage.setItem('autoExpandTools', JSON.stringify(value));
}}
onAutoExpandChange={setAutoExpandTools}
showRawParameters={showRawParameters}
onShowRawParametersChange={(value) => {
setShowRawParameters(value);
localStorage.setItem('showRawParameters', JSON.stringify(value));
}}
onShowRawParametersChange={setShowRawParameters}
autoScrollToBottom={autoScrollToBottom}
onAutoScrollChange={(value) => {
setAutoScrollToBottom(value);
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
}}
onAutoScrollChange={setAutoScrollToBottom}
sendByCtrlEnter={sendByCtrlEnter}
onSendByCtrlEnterChange={(value) => {
setSendByCtrlEnter(value);
localStorage.setItem('sendByCtrlEnter', JSON.stringify(value));
}}
onSendByCtrlEnterChange={setSendByCtrlEnter}
isMobile={isMobile}
/>
)}

View File

@@ -157,9 +157,11 @@ const safeLocalStorage = {
// Memoized message component to prevent unnecessary re-renders
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 isGrouped = prevMessage && prevMessage.type === message.type &&
((prevMessage.type === 'assistant') ||
(prevMessage.type === 'user') ||
(prevMessage.type === 'tool') ||
(prevMessage.type === 'error'));
const messageRef = React.useRef(null);
const [isExpanded, setIsExpanded] = React.useState(false);
React.useEffect(() => {
@@ -3217,7 +3219,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
{/* Input Area - Fixed Bottom */}
<div className={`p-2 sm:p-4 md:p-4 flex-shrink-0 ${
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6'
isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-2 sm:pb-4 md:pb-6'
}`}>
<div className="flex-1">

View File

@@ -0,0 +1,41 @@
import React from 'react';
function DiffViewer({ diff, fileName, isMobile, wrapText }) {
if (!diff) {
return (
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
No diff available
</div>
);
}
const renderDiffLine = (line, index) => {
const isAddition = line.startsWith('+') && !line.startsWith('+++');
const isDeletion = line.startsWith('-') && !line.startsWith('---');
const isHeader = line.startsWith('@@');
return (
<div
key={index}
className={`font-mono text-xs p-2 ${
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
} ${
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
'text-gray-600 dark:text-gray-400'
}`}
>
{line}
</div>
);
};
return (
<div className="diff-viewer">
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
</div>
);
}
export default DiffViewer;

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button';
import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye } from 'lucide-react';
import { Input } from './ui/input';
import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X } from 'lucide-react';
import { cn } from '../lib/utils';
import CodeEditor from './CodeEditor';
import ImageViewer from './ImageViewer';
@@ -14,6 +15,8 @@ function FileTree({ selectedProject }) {
const [selectedFile, setSelectedFile] = useState(null);
const [selectedImage, setSelectedImage] = useState(null);
const [viewMode, setViewMode] = useState('detailed'); // 'simple', 'detailed', 'compact'
const [searchQuery, setSearchQuery] = useState('');
const [filteredFiles, setFilteredFiles] = useState([]);
useEffect(() => {
if (selectedProject) {
@@ -29,6 +32,51 @@ function FileTree({ selectedProject }) {
}
}, []);
// Filter files based on search query
useEffect(() => {
if (!searchQuery.trim()) {
setFilteredFiles(files);
} else {
const filtered = filterFiles(files, searchQuery.toLowerCase());
setFilteredFiles(filtered);
// Auto-expand directories that contain matches
const expandMatches = (items) => {
items.forEach(item => {
if (item.type === 'directory' && item.children && item.children.length > 0) {
setExpandedDirs(prev => new Set(prev.add(item.path)));
expandMatches(item.children);
}
});
};
expandMatches(filtered);
}
}, [files, searchQuery]);
// Recursively filter files and directories based on search query
const filterFiles = (items, query) => {
return items.reduce((filtered, item) => {
const matchesName = item.name.toLowerCase().includes(query);
let filteredChildren = [];
if (item.type === 'directory' && item.children) {
filteredChildren = filterFiles(item.children, query);
}
// Include item if:
// 1. It matches the search query, or
// 2. It's a directory with matching children
if (matchesName || filteredChildren.length > 0) {
filtered.push({
...item,
children: filteredChildren
});
}
return filtered;
}, []);
};
const fetchFiles = async () => {
setLoading(true);
try {
@@ -308,42 +356,67 @@ function FileTree({ selectedProject }) {
return (
<div className="h-full flex flex-col bg-card">
{/* View Mode Toggle */}
<div className="p-4 border-b border-border flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground">Files</h3>
<div className="flex gap-1">
<Button
variant={viewMode === 'simple' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('simple')}
title="Simple view"
>
<List className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'compact' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('compact')}
title="Compact view"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('detailed')}
title="Detailed view"
>
<TableProperties className="w-4 h-4" />
</Button>
{/* Header with Search and View Mode Toggle */}
<div className="p-4 border-b border-border space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-foreground">Files</h3>
<div className="flex gap-1">
<Button
variant={viewMode === 'simple' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('simple')}
title="Simple view"
>
<List className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'compact' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('compact')}
title="Compact view"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
size="sm"
className="h-8 w-8 p-0"
onClick={() => changeViewMode('detailed')}
title="Detailed view"
>
<TableProperties className="w-4 h-4" />
</Button>
</div>
</div>
{/* Search Bar */}
<div className="relative">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search files and folders..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 pr-8 h-8 text-sm"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0 hover:bg-accent"
onClick={() => setSearchQuery('')}
title="Clear search"
>
<X className="w-3 h-3" />
</Button>
)}
</div>
</div>
{/* Column Headers for Detailed View */}
{viewMode === 'detailed' && files.length > 0 && (
{viewMode === 'detailed' && filteredFiles.length > 0 && (
<div className="px-4 pt-2 pb-1 border-b border-border">
<div className="grid grid-cols-12 gap-2 px-2 text-xs font-medium text-muted-foreground">
<div className="col-span-5">Name</div>
@@ -365,11 +438,21 @@ function FileTree({ selectedProject }) {
Check if the project path is accessible
</p>
</div>
) : filteredFiles.length === 0 && searchQuery ? (
<div className="text-center py-8">
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
<Search className="w-6 h-6 text-muted-foreground" />
</div>
<h4 className="font-medium text-foreground mb-1">No matches found</h4>
<p className="text-sm text-muted-foreground">
Try a different search term or clear the search
</p>
</div>
) : (
<div className={viewMode === 'detailed' ? '' : 'space-y-1'}>
{viewMode === 'simple' && renderFileTree(files)}
{viewMode === 'compact' && renderCompactView(files)}
{viewMode === 'detailed' && renderDetailedView(files)}
{viewMode === 'simple' && renderFileTree(filteredFiles)}
{viewMode === 'compact' && renderCompactView(filteredFiles)}
{viewMode === 'detailed' && renderDetailedView(filteredFiles)}
</div>
)}
</ScrollArea>

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle, Upload } from 'lucide-react';
import { MicButton } from './MicButton.jsx';
import { authenticatedFetch } from '../utils/api';
import DiffViewer from './DiffViewer.jsx';
function GitPanel({ selectedProject, isMobile }) {
const [gitStatus, setGitStatus] = useState(null);
@@ -523,27 +524,6 @@ function GitPanel({ selectedProject, isMobile }) {
}
};
const renderDiffLine = (line, index) => {
const isAddition = line.startsWith('+') && !line.startsWith('+++');
const isDeletion = line.startsWith('-') && !line.startsWith('---');
const isHeader = line.startsWith('@@');
return (
<div
key={index}
className={`font-mono text-xs ${
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
} ${
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
'text-gray-600 dark:text-gray-400'
}`}
>
{line}
</div>
);
};
const getStatusLabel = (status) => {
switch (status) {
@@ -590,7 +570,7 @@ function GitPanel({ selectedProject, isMobile }) {
<div className="text-xs font-mono text-gray-600 dark:text-gray-400 mb-2">
{commit.stats}
</div>
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
<DiffViewer diff={diff} fileName="commit" isMobile={isMobile} wrapText={wrapText} />
</div>
</div>
)}
@@ -705,8 +685,8 @@ function GitPanel({ selectedProject, isMobile }) {
</button>
)}
</div>
<div className="max-h-96 overflow-y-auto p-2">
{diff && diff.split('\n').map((line, index) => renderDiffLine(line, index))}
<div className="max-h-96 overflow-y-auto">
{diff && <DiffViewer diff={diff} fileName={filePath} isMobile={isMobile} wrapText={wrapText} />}
</div>
</div>
</div>

View File

@@ -37,6 +37,7 @@ function MainContent({
sendMessage,
messages,
isMobile,
isPWA,
onMenuClick,
isLoading,
onInputFocusChange,
@@ -152,10 +153,12 @@ function MainContent({
<div className="h-full flex flex-col">
{/* Header with menu button for mobile */}
{isMobile && (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
>
<button
onClick={onMenuClick}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
@@ -188,10 +191,12 @@ function MainContent({
<div className="h-full flex flex-col">
{/* Header with menu button for mobile */}
{isMobile && (
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
>
<button
onClick={onMenuClick}
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-1.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 pwa-menu-button"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
@@ -224,7 +229,9 @@ function MainContent({
return (
<div className="h-full flex flex-col">
{/* Header with tabs */}
<div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 flex-shrink-0">
<div
className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-3 sm:p-4 pwa-header-safe flex-shrink-0"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 sm:space-x-3">
{isMobile && (
@@ -234,7 +241,7 @@ function MainContent({
e.preventDefault();
onMenuClick();
}}
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95"
className="p-2.5 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 touch-manipulation active:scale-95 pwa-menu-button"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />

View File

@@ -1992,8 +1992,8 @@ function Settings({ isOpen, onClose, projects = [] }) {
{/* Login Modal */}
{showLoginModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col">
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 max-md:items-stretch max-md:justify-stretch">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col md:max-w-4xl md:h-3/4 md:rounded-lg md:m-4 max-md:max-w-none max-md:h-full max-md:rounded-none max-md:m-0">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{loginProvider === 'claude' ? 'Claude CLI Login' : 'Cursor CLI Login'}

View File

@@ -54,7 +54,9 @@ function Sidebar({
updateAvailable,
latestVersion,
currentVersion,
onShowVersionModal
onShowVersionModal,
isPWA,
isMobile
}) {
const [expandedProjects, setExpandedProjects] = useState(new Set());
const [editingProject, setEditingProject] = useState(null);
@@ -562,7 +564,10 @@ function Sidebar({
};
return (
<div className="h-full flex flex-col bg-card md:select-none">
<div
className="h-full flex flex-col bg-card md:select-none"
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
>
{/* Header */}
<div className="md:p-4 md:border-b md:border-border">
{/* Desktop Header */}
@@ -607,7 +612,10 @@ function Sidebar({
</div>
{/* Mobile Header */}
<div className="md:hidden p-3 border-b border-border">
<div
className="md:hidden p-3 border-b border-border"
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
/**
* Custom hook to persist state in localStorage.
*
* @param {string} key The key to use for localStorage.
* @param {any} initialValue The initial value to use if nothing is in localStorage.
* @returns {[any, Function]} A tuple containing the stored value and a setter function.
*/
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value) => {
if (typeof window === 'undefined') {
return;
}
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;

View File

@@ -43,6 +43,22 @@
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
/* Safe area CSS variables */
--safe-area-inset-top: env(safe-area-inset-top);
--safe-area-inset-right: env(safe-area-inset-right);
--safe-area-inset-bottom: env(safe-area-inset-bottom);
--safe-area-inset-left: env(safe-area-inset-left);
}
/* Fallback for older iOS versions */
@supports (padding-top: constant(safe-area-inset-top)) {
:root {
--safe-area-inset-top: constant(safe-area-inset-top);
--safe-area-inset-right: constant(safe-area-inset-right);
--safe-area-inset-bottom: constant(safe-area-inset-bottom);
--safe-area-inset-left: constant(safe-area-inset-left);
}
}
.dark {
@@ -82,12 +98,60 @@
padding: 0;
}
html, body, #root {
html, body {
height: 100%;
margin: 0;
padding: 0;
}
/* Root element with safe area padding for PWA */
#root {
min-height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Apply safe area padding in standalone mode */
@supports (padding-top: env(safe-area-inset-top)) {
@media (display-mode: standalone) {
#root {
padding-top: var(--safe-area-inset-top);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
}
}
/* PWA mode detected by JavaScript - more reliable */
html.pwa-mode,
body.pwa-mode {
height: 100%;
overflow: hidden;
background-color: rgb(255 255 255); /* white - same as bg-white for safe area */
}
body.pwa-mode #root {
padding-top: calc(env(safe-area-inset-top, 0px) + 8px);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
height: 100vh;
overflow: hidden;
}
/* Adjust fixed inset positioning in PWA mode */
body.pwa-mode .fixed.inset-0 {
top: calc(env(safe-area-inset-top, 0px) + 8px);
left: env(safe-area-inset-left);
right: env(safe-area-inset-right);
bottom: 0;
}
/* Dark mode safe area background */
html.dark body.pwa-mode {
background-color: rgb(31 41 55); /* gray-800 - matches header color */
}
/* Global transition defaults */
button,
a,
@@ -577,6 +641,25 @@
padding-bottom: max(env(safe-area-inset-bottom), 12px);
}
/* PWA specific header adjustments for iOS */
.pwa-header-safe {
padding-top: 16px;
}
/* When PWA mode is detected by JavaScript */
body.pwa-mode .pwa-header-safe {
/* Reset padding since #root already handles safe area */
padding-top: 0px !important;
}
/* For mobile PWA, ensure proper header spacing */
@media screen and (max-width: 768px) {
body.pwa-mode .pwa-header-safe {
padding-top: 4px !important;
padding-bottom: 12px;
}
}
@media screen and (max-width: 768px) {
.chat-input-mobile {
padding-bottom: calc(60px + max(env(safe-area-inset-bottom), 12px));