mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-12 21:29:42 +00:00
Compare commits
13 Commits
v1.10.0
...
72e97c4fbc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72e97c4fbc | ||
|
|
b5d1fed354 | ||
|
|
fefcc0f338 | ||
|
|
36f8f50d63 | ||
|
|
4e14222487 | ||
|
|
e2ba000e86 | ||
|
|
64e2909f0f | ||
|
|
6541760eb7 | ||
|
|
50454175c9 | ||
|
|
d6ceb222c3 | ||
|
|
9cfb7e659d | ||
|
|
018b337871 | ||
|
|
0b8b1d0677 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.10.0",
|
"version": "1.10.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.10.0",
|
"version": "1.10.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.1.29",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.29",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@siteboon/claude-code-ui",
|
"name": "@siteboon/claude-code-ui",
|
||||||
"version": "1.10.0",
|
"version": "1.10.5",
|
||||||
"description": "A web-based UI for Claude Code CLI",
|
"description": "A web-based UI for Claude Code CLI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "npm run build && npm run server",
|
"start": "npm run build && npm run server",
|
||||||
"release": "release-it"
|
"release": "./release.sh"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude coode",
|
"claude coode",
|
||||||
|
|||||||
4
release.sh
Executable file
4
release.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Load environment variables from .env
|
||||||
|
export $(grep -v '^#' .env | grep '^GITHUB_TOKEN=' | xargs)
|
||||||
|
exec npx release-it "$@"
|
||||||
@@ -79,6 +79,16 @@ function mapCliOptionsToSDK(options = {}) {
|
|||||||
// Map model (default to sonnet)
|
// Map model (default to sonnet)
|
||||||
sdkOptions.model = options.model || 'sonnet';
|
sdkOptions.model = options.model || 'sonnet';
|
||||||
|
|
||||||
|
// Map system prompt configuration
|
||||||
|
sdkOptions.systemPrompt = {
|
||||||
|
type: 'preset',
|
||||||
|
preset: 'claude_code' // Required to use CLAUDE.md
|
||||||
|
};
|
||||||
|
|
||||||
|
// Map setting sources for CLAUDE.md loading
|
||||||
|
// This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
|
||||||
|
sdkOptions.settingSources = ['project', 'user', 'local'];
|
||||||
|
|
||||||
// Map resume session
|
// Map resume session
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
sdkOptions.resume = sessionId;
|
sdkOptions.resume = sessionId;
|
||||||
@@ -374,7 +384,7 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|||||||
for await (const message of queryInstance) {
|
for await (const message of queryInstance) {
|
||||||
// Capture session ID from first message
|
// Capture session ID from first message
|
||||||
if (message.session_id && !capturedSessionId) {
|
if (message.session_id && !capturedSessionId) {
|
||||||
console.log('📝 Captured session ID:', message.session_id);
|
|
||||||
capturedSessionId = message.session_id;
|
capturedSessionId = message.session_id;
|
||||||
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,71 @@ app.get('/api/config', authenticateToken, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// System update endpoint
|
||||||
|
app.post('/api/system/update', authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Get the project root directory (parent of server directory)
|
||||||
|
const projectRoot = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
console.log('Starting system update from directory:', projectRoot);
|
||||||
|
|
||||||
|
// Run the update command
|
||||||
|
const updateCommand = 'git checkout main && git pull && npm install';
|
||||||
|
|
||||||
|
const child = spawn('sh', ['-c', updateCommand], {
|
||||||
|
cwd: projectRoot,
|
||||||
|
env: process.env
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let errorOutput = '';
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
const text = data.toString();
|
||||||
|
output += text;
|
||||||
|
console.log('Update output:', text);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
const text = data.toString();
|
||||||
|
errorOutput += text;
|
||||||
|
console.error('Update error:', text);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
output: output || 'Update completed successfully',
|
||||||
|
message: 'Update completed. Please restart the server to apply changes.'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Update command failed',
|
||||||
|
output: output,
|
||||||
|
errorOutput: errorOutput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (error) => {
|
||||||
|
console.error('Update process error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('System update error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/projects', authenticateToken, async (req, res) => {
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const projects = await getProjects();
|
const projects = await getProjects();
|
||||||
|
|||||||
@@ -90,13 +90,13 @@ function normalizeGitHubUrl(url) {
|
|||||||
function parseGitHubUrl(url) {
|
function parseGitHubUrl(url) {
|
||||||
// Handle HTTPS URLs: https://github.com/owner/repo or https://github.com/owner/repo.git
|
// Handle HTTPS URLs: https://github.com/owner/repo or https://github.com/owner/repo.git
|
||||||
// Handle SSH URLs: git@github.com:owner/repo or git@github.com:owner/repo.git
|
// Handle SSH URLs: git@github.com:owner/repo or git@github.com:owner/repo.git
|
||||||
const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error('Invalid GitHub URL format');
|
throw new Error('Invalid GitHub URL format');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
owner: match[1],
|
owner: match[1],
|
||||||
repo: match[2].replace('.git', '')
|
repo: match[2].replace(/\.git$/, '')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,14 +114,37 @@ function autogenerateBranchName(message) {
|
|||||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||||
|
|
||||||
// Limit length to 50 characters
|
// Ensure non-empty fallback
|
||||||
if (branchName.length > 50) {
|
if (!branchName) {
|
||||||
branchName = branchName.substring(0, 50).replace(/-$/, '');
|
branchName = 'task';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add timestamp suffix to ensure uniqueness
|
// Generate timestamp suffix (last 6 chars of base36 timestamp)
|
||||||
const timestamp = Date.now().toString(36).substring(-6);
|
const timestamp = Date.now().toString(36).slice(-6);
|
||||||
branchName = `${branchName}-${timestamp}`;
|
const suffix = `-${timestamp}`;
|
||||||
|
|
||||||
|
// Limit length to ensure total length including suffix fits within 50 characters
|
||||||
|
const maxBaseLength = 50 - suffix.length;
|
||||||
|
if (branchName.length > maxBaseLength) {
|
||||||
|
branchName = branchName.substring(0, maxBaseLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any trailing hyphen after truncation and ensure no leading hyphen
|
||||||
|
branchName = branchName.replace(/-$/, '').replace(/^-+/, '');
|
||||||
|
|
||||||
|
// If still empty or starts with hyphen after cleanup, use fallback
|
||||||
|
if (!branchName || branchName.startsWith('-')) {
|
||||||
|
branchName = 'task';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine base name with timestamp suffix
|
||||||
|
branchName = `${branchName}${suffix}`;
|
||||||
|
|
||||||
|
// Final validation: ensure it matches safe pattern
|
||||||
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(branchName)) {
|
||||||
|
// Fallback to deterministic safe name
|
||||||
|
return `branch-${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
return branchName;
|
return branchName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -502,16 +502,16 @@ router.post('/generate-commit-message', async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
|
async function generateCommitMessageWithAI(files, diffContext, provider, projectPath) {
|
||||||
// Create the prompt
|
// Create the prompt
|
||||||
const prompt = `You are a git commit message generator. Based on the following file changes and diffs, generate a commit message in conventional commit format.
|
const prompt = `Generate a conventional commit message for these changes.
|
||||||
|
|
||||||
REQUIREMENTS:
|
REQUIREMENTS:
|
||||||
- Use conventional commit format: type(scope): subject
|
- Format: type(scope): subject
|
||||||
- Include a body that explains what changed and why
|
- Include body explaining what changed and why
|
||||||
- Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
|
||||||
- Keep subject line under 50 characters
|
- Subject under 50 chars, body wrapped at 72 chars
|
||||||
- Wrap body at 72 characters
|
- Focus on user-facing changes, not implementation details
|
||||||
- Be specific and descriptive
|
- Consider what's being added AND removed
|
||||||
- Return ONLY the commit message, nothing else - no markdown, no explanations, no code blocks
|
- Return ONLY the commit message (no markdown, explanations, or code blocks)
|
||||||
|
|
||||||
FILES CHANGED:
|
FILES CHANGED:
|
||||||
${files.map(f => `- ${f}`).join('\n')}
|
${files.map(f => `- ${f}`).join('\n')}
|
||||||
@@ -519,7 +519,7 @@ ${files.map(f => `- ${f}`).join('\n')}
|
|||||||
DIFFS:
|
DIFFS:
|
||||||
${diffContext.substring(0, 4000)}
|
${diffContext.substring(0, 4000)}
|
||||||
|
|
||||||
Generate the commit message now:`;
|
Generate the commit message:`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a simple writer that collects the response
|
// Create a simple writer that collects the response
|
||||||
|
|||||||
194
src/App.jsx
194
src/App.jsx
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { Settings as SettingsIcon, Sparkles } from 'lucide-react';
|
||||||
import Sidebar from './components/Sidebar';
|
import Sidebar from './components/Sidebar';
|
||||||
import MainContent from './components/MainContent';
|
import MainContent from './components/MainContent';
|
||||||
import MobileNav from './components/MobileNav';
|
import MobileNav from './components/MobileNav';
|
||||||
@@ -42,7 +43,7 @@ function AppContent() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { sessionId } = useParams();
|
const { sessionId } = useParams();
|
||||||
|
|
||||||
const { updateAvailable, latestVersion, currentVersion } = useVersionCheck('siteboon', 'claudecodeui');
|
const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui');
|
||||||
const [showVersionModal, setShowVersionModal] = useState(false);
|
const [showVersionModal, setShowVersionModal] = useState(false);
|
||||||
|
|
||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
@@ -54,12 +55,14 @@ function AppContent() {
|
|||||||
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const [settingsInitialTab, setSettingsInitialTab] = useState('tools');
|
||||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||||
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
|
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
|
||||||
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
|
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
|
||||||
const [showThinking, setShowThinking] = useLocalStorage('showThinking', true);
|
const [showThinking, setShowThinking] = useLocalStorage('showThinking', true);
|
||||||
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
|
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
|
||||||
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false);
|
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false);
|
||||||
|
const [sidebarVisible, setSidebarVisible] = useLocalStorage('sidebarVisible', true);
|
||||||
// Session Protection System: Track sessions with active conversations to prevent
|
// Session Protection System: Track sessions with active conversations to prevent
|
||||||
// automatic project updates from interrupting ongoing chats. When a user sends
|
// automatic project updates from interrupting ongoing chats. When a user sends
|
||||||
// a message, the session is marked as "active" and project updates are paused
|
// a message, the session is marked as "active" and project updates are paused
|
||||||
@@ -306,6 +309,12 @@ function AppContent() {
|
|||||||
// Expose fetchProjects globally for component access
|
// Expose fetchProjects globally for component access
|
||||||
window.refreshProjects = fetchProjects;
|
window.refreshProjects = fetchProjects;
|
||||||
|
|
||||||
|
// Expose openSettings function globally for component access
|
||||||
|
window.openSettings = useCallback((tab = 'tools') => {
|
||||||
|
setSettingsInitialTab(tab);
|
||||||
|
setShowSettings(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle URL-based session loading
|
// Handle URL-based session loading
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionId && projects.length > 0) {
|
if (sessionId && projects.length > 0) {
|
||||||
@@ -536,8 +545,60 @@ function AppContent() {
|
|||||||
|
|
||||||
// Version Upgrade Modal Component
|
// Version Upgrade Modal Component
|
||||||
const VersionUpgradeModal = () => {
|
const VersionUpgradeModal = () => {
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [updateOutput, setUpdateOutput] = useState('');
|
||||||
|
const [updateError, setUpdateError] = useState('');
|
||||||
|
|
||||||
if (!showVersionModal) return null;
|
if (!showVersionModal) return null;
|
||||||
|
|
||||||
|
// Clean up changelog by removing GitHub-specific metadata
|
||||||
|
const cleanChangelog = (body) => {
|
||||||
|
if (!body) return '';
|
||||||
|
|
||||||
|
return body
|
||||||
|
// Remove full commit hashes (40 character hex strings)
|
||||||
|
.replace(/\b[0-9a-f]{40}\b/gi, '')
|
||||||
|
// Remove short commit hashes (7-10 character hex strings at start of line or after dash/space)
|
||||||
|
.replace(/(?:^|\s|-)([0-9a-f]{7,10})\b/gi, '')
|
||||||
|
// Remove "Full Changelog" links
|
||||||
|
.replace(/\*\*Full Changelog\*\*:.*$/gim, '')
|
||||||
|
// Remove compare links (e.g., https://github.com/.../compare/v1.0.0...v1.0.1)
|
||||||
|
.replace(/https?:\/\/github\.com\/[^\/]+\/[^\/]+\/compare\/[^\s)]+/gi, '')
|
||||||
|
// Clean up multiple consecutive empty lines
|
||||||
|
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||||
|
// Trim whitespace
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateNow = async () => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
setUpdateOutput('Starting update...\n');
|
||||||
|
setUpdateError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the backend API to run the update command
|
||||||
|
const response = await authenticatedFetch('/api/system/update', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setUpdateOutput(prev => prev + data.output + '\n');
|
||||||
|
setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n');
|
||||||
|
setUpdateOutput(prev => prev + 'Please restart the server to apply changes.\n');
|
||||||
|
} else {
|
||||||
|
setUpdateError(data.error || 'Update failed');
|
||||||
|
setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setUpdateError(error.message);
|
||||||
|
setUpdateOutput(prev => prev + '\n❌ Update failed: ' + error.message + '\n');
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
@@ -548,7 +609,7 @@ function AppContent() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 p-6 space-y-4">
|
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-2xl mx-4 p-6 space-y-4 max-h-[90vh] overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -559,7 +620,9 @@ function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Update Available</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Update Available</h2>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">A new version is ready</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{releaseInfo?.title || 'A new version is ready'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -584,18 +647,57 @@ function AppContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upgrade Instructions */}
|
{/* Changelog */}
|
||||||
|
{releaseInfo?.body && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">How to upgrade:</h3>
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white">What's New:</h3>
|
||||||
|
{releaseInfo?.htmlUrl && (
|
||||||
|
<a
|
||||||
|
href={releaseInfo.htmlUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
View full release
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 border border-gray-200 dark:border-gray-600 max-h-64 overflow-y-auto">
|
||||||
|
<div className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
{cleanChangelog(releaseInfo.body)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Update Output */}
|
||||||
|
{updateOutput && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Update Progress:</h3>
|
||||||
|
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 border border-gray-700 max-h-48 overflow-y-auto">
|
||||||
|
<pre className="text-xs text-green-400 font-mono whitespace-pre-wrap">{updateOutput}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upgrade Instructions */}
|
||||||
|
{!isUpdating && !updateOutput && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white">Manual upgrade:</h3>
|
||||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
||||||
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
||||||
git checkout main && git pull && npm install
|
git checkout main && git pull && npm install
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
Run this command in your Claude Code UI directory to update to the latest version.
|
Or click "Update Now" to run the update automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2 pt-2">
|
<div className="flex gap-2 pt-2">
|
||||||
@@ -603,18 +705,34 @@ function AppContent() {
|
|||||||
onClick={() => setShowVersionModal(false)}
|
onClick={() => setShowVersionModal(false)}
|
||||||
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
Later
|
{updateOutput ? 'Close' : 'Later'}
|
||||||
</button>
|
</button>
|
||||||
|
{!updateOutput && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Copy command to clipboard
|
|
||||||
navigator.clipboard.writeText('git checkout main && git pull && npm install');
|
navigator.clipboard.writeText('git checkout main && git pull && npm install');
|
||||||
setShowVersionModal(false);
|
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
Copy Command
|
Copy Command
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleUpdateNow}
|
||||||
|
disabled={isUpdating}
|
||||||
|
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed rounded-md transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{isUpdating ? (
|
||||||
|
<>
|
||||||
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
Updating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Update Now'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -625,8 +743,13 @@ function AppContent() {
|
|||||||
<div className="fixed inset-0 flex bg-background">
|
<div className="fixed inset-0 flex bg-background">
|
||||||
{/* Fixed Desktop Sidebar */}
|
{/* Fixed Desktop Sidebar */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="w-80 flex-shrink-0 border-r border-border bg-card">
|
<div
|
||||||
|
className={`flex-shrink-0 border-r border-border bg-card transition-all duration-300 ${
|
||||||
|
sidebarVisible ? 'w-80' : 'w-14'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="h-full overflow-hidden">
|
<div className="h-full overflow-hidden">
|
||||||
|
{sidebarVisible ? (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
projects={projects}
|
projects={projects}
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
@@ -642,10 +765,56 @@ function AppContent() {
|
|||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
|
releaseInfo={releaseInfo}
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
isPWA={isPWA}
|
isPWA={isPWA}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
onToggleSidebar={() => setSidebarVisible(false)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
/* Collapsed Sidebar */
|
||||||
|
<div className="h-full flex flex-col items-center py-4 gap-4">
|
||||||
|
{/* Expand Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSidebarVisible(true)}
|
||||||
|
className="p-2 hover:bg-accent rounded-md transition-colors duration-200 group"
|
||||||
|
aria-label="Show sidebar"
|
||||||
|
title="Show sidebar"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-foreground group-hover:scale-110 transition-transform"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Settings Icon */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
className="p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
||||||
|
aria-label="Settings"
|
||||||
|
title="Settings"
|
||||||
|
>
|
||||||
|
<SettingsIcon className="w-5 h-5 text-muted-foreground hover:text-foreground transition-colors" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Update Indicator */}
|
||||||
|
{updateAvailable && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowVersionModal(true)}
|
||||||
|
className="relative p-2 hover:bg-accent rounded-md transition-colors duration-200"
|
||||||
|
aria-label="Update available"
|
||||||
|
title="Update available"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-5 h-5 text-blue-500" />
|
||||||
|
<span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -691,9 +860,11 @@ function AppContent() {
|
|||||||
updateAvailable={updateAvailable}
|
updateAvailable={updateAvailable}
|
||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
currentVersion={currentVersion}
|
currentVersion={currentVersion}
|
||||||
|
releaseInfo={releaseInfo}
|
||||||
onShowVersionModal={() => setShowVersionModal(true)}
|
onShowVersionModal={() => setShowVersionModal(true)}
|
||||||
isPWA={isPWA}
|
isPWA={isPWA}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
onToggleSidebar={() => setSidebarVisible(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -763,6 +934,7 @@ function AppContent() {
|
|||||||
isOpen={showSettings}
|
isOpen={showSettings}
|
||||||
onClose={() => setShowSettings(false)}
|
onClose={() => setShowSettings(false)}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
|
initialTab={settingsInitialTab}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Version Upgrade Modal */}
|
{/* Version Upgrade Modal */}
|
||||||
|
|||||||
@@ -514,7 +514,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="text-lg leading-none">📝</span>
|
|
||||||
<span>View edit diff for</span>
|
<span>View edit diff for</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -2604,7 +2603,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||||
setIsUserScrolledUp(false);
|
// Don't reset isUserScrolledUp here - let the scroll handler manage it
|
||||||
|
// This prevents fighting with user's scroll position during streaming
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -3537,9 +3537,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
if (scrollContainerRef.current && chatMessages.length > 0 && !isLoadingSessionRef.current) {
|
if (scrollContainerRef.current && chatMessages.length > 0 && !isLoadingSessionRef.current) {
|
||||||
// Only scroll if we're not in the middle of loading a session
|
// Only scroll if we're not in the middle of loading a session
|
||||||
// This prevents the "double scroll" effect during session switching
|
// This prevents the "double scroll" effect during session switching
|
||||||
// Also reset scroll state
|
// Reset scroll state when switching sessions
|
||||||
setIsUserScrolledUp(false);
|
setIsUserScrolledUp(false);
|
||||||
setTimeout(() => scrollToBottom(), 200); // Delay to ensure full rendering
|
setTimeout(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
// After scrolling, the scroll event handler will naturally set isUserScrolledUp based on position
|
||||||
|
}, 200); // Delay to ensure full rendering
|
||||||
}
|
}
|
||||||
}, [selectedSession?.id, selectedProject?.name]); // Only trigger when session/project changes
|
}, [selectedSession?.id, selectedProject?.name]); // Only trigger when session/project changes
|
||||||
|
|
||||||
|
|||||||
@@ -10,23 +10,37 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
|||||||
import { EditorView, showPanel, ViewPlugin } from '@codemirror/view';
|
import { EditorView, showPanel, ViewPlugin } from '@codemirror/view';
|
||||||
import { unifiedMergeView, getChunks } from '@codemirror/merge';
|
import { unifiedMergeView, getChunks } from '@codemirror/merge';
|
||||||
import { showMinimap } from '@replit/codemirror-minimap';
|
import { showMinimap } from '@replit/codemirror-minimap';
|
||||||
import { X, Save, Download, Maximize2, Minimize2, Eye, EyeOff } from 'lucide-react';
|
import { X, Save, Download, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
|
||||||
function CodeEditor({ file, onClose, projectPath }) {
|
function CodeEditor({ file, onClose, projectPath, isSidebar = false, isExpanded = false, onToggleExpand = null }) {
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [isDarkMode, setIsDarkMode] = useState(true);
|
const [isDarkMode, setIsDarkMode] = useState(() => {
|
||||||
|
const savedTheme = localStorage.getItem('codeEditorTheme');
|
||||||
|
return savedTheme ? savedTheme === 'dark' : true;
|
||||||
|
});
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
const [showDiff, setShowDiff] = useState(!!file.diffInfo);
|
const [showDiff, setShowDiff] = useState(!!file.diffInfo);
|
||||||
const [wordWrap, setWordWrap] = useState(false);
|
const [wordWrap, setWordWrap] = useState(() => {
|
||||||
|
return localStorage.getItem('codeEditorWordWrap') === 'true';
|
||||||
|
});
|
||||||
|
const [minimapEnabled, setMinimapEnabled] = useState(() => {
|
||||||
|
return localStorage.getItem('codeEditorShowMinimap') !== 'false';
|
||||||
|
});
|
||||||
|
const [showLineNumbers, setShowLineNumbers] = useState(() => {
|
||||||
|
return localStorage.getItem('codeEditorLineNumbers') !== 'false';
|
||||||
|
});
|
||||||
|
const [fontSize, setFontSize] = useState(() => {
|
||||||
|
return localStorage.getItem('codeEditorFontSize') || '14';
|
||||||
|
});
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
// Create minimap extension with chunk-based gutters
|
// Create minimap extension with chunk-based gutters
|
||||||
const minimapExtension = useMemo(() => {
|
const minimapExtension = useMemo(() => {
|
||||||
if (!file.diffInfo || !showDiff) return [];
|
if (!file.diffInfo || !showDiff || !minimapEnabled) return [];
|
||||||
|
|
||||||
const gutters = {};
|
const gutters = {};
|
||||||
|
|
||||||
@@ -58,7 +72,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}, [file.diffInfo, showDiff, isDarkMode]);
|
}, [file.diffInfo, showDiff, minimapEnabled, isDarkMode]);
|
||||||
|
|
||||||
// Create extension to scroll to first chunk on mount
|
// Create extension to scroll to first chunk on mount
|
||||||
const scrollToFirstChunkExtension = useMemo(() => {
|
const scrollToFirstChunkExtension = useMemo(() => {
|
||||||
@@ -89,24 +103,28 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
];
|
];
|
||||||
}, [file.diffInfo, showDiff]);
|
}, [file.diffInfo, showDiff]);
|
||||||
|
|
||||||
// Create diff navigation panel extension
|
// Create editor toolbar panel - always visible
|
||||||
const diffNavigationPanel = useMemo(() => {
|
const editorToolbarPanel = useMemo(() => {
|
||||||
if (!file.diffInfo || !showDiff) return [];
|
|
||||||
|
|
||||||
const createPanel = (view) => {
|
const createPanel = (view) => {
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div');
|
||||||
dom.className = 'cm-diff-navigation-panel';
|
dom.className = 'cm-editor-toolbar-panel';
|
||||||
|
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
|
|
||||||
const updatePanel = () => {
|
const updatePanel = () => {
|
||||||
// Use getChunks API to get ALL chunks regardless of viewport
|
// Check if we have diff info and it's enabled
|
||||||
const chunksData = getChunks(view.state);
|
const hasDiff = file.diffInfo && showDiff;
|
||||||
|
const chunksData = hasDiff ? getChunks(view.state) : null;
|
||||||
const chunks = chunksData?.chunks || [];
|
const chunks = chunksData?.chunks || [];
|
||||||
const chunkCount = chunks.length;
|
const chunkCount = chunks.length;
|
||||||
|
|
||||||
dom.innerHTML = `
|
// Build the toolbar HTML
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
let toolbarHTML = '<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">';
|
||||||
|
|
||||||
|
// Left side - diff navigation (if applicable)
|
||||||
|
toolbarHTML += '<div style="display: flex; align-items: center; gap: 8px;">';
|
||||||
|
if (hasDiff) {
|
||||||
|
toolbarHTML += `
|
||||||
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span>
|
<span style="font-weight: 500;">${chunkCount > 0 ? `${currentIndex + 1}/${chunkCount}` : '0'} changes</span>
|
||||||
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}>
|
<button class="cm-diff-nav-btn cm-diff-nav-prev" title="Previous change" ${chunkCount === 0 ? 'disabled' : ''}>
|
||||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@@ -118,9 +136,57 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
`;
|
||||||
|
}
|
||||||
|
toolbarHTML += '</div>';
|
||||||
|
|
||||||
|
// Right side - action buttons
|
||||||
|
toolbarHTML += '<div style="display: flex; align-items: center; gap: 4px;">';
|
||||||
|
|
||||||
|
// Show/hide diff button (only if there's diff info)
|
||||||
|
if (file.diffInfo) {
|
||||||
|
toolbarHTML += `
|
||||||
|
<button class="cm-toolbar-btn cm-toggle-diff-btn" title="${showDiff ? 'Hide diff highlighting' : 'Show diff highlighting'}">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
${showDiff ?
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />' :
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />'
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings button
|
||||||
|
toolbarHTML += `
|
||||||
|
<button class="cm-toolbar-btn cm-settings-btn" title="Editor Settings">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Expand button (only in sidebar mode)
|
||||||
|
if (isSidebar && onToggleExpand) {
|
||||||
|
toolbarHTML += `
|
||||||
|
<button class="cm-toolbar-btn cm-expand-btn" title="${isExpanded ? 'Collapse editor' : 'Expand editor to full width'}">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
${isExpanded ?
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" />' :
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" />'
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbarHTML += '</div>';
|
||||||
|
toolbarHTML += '</div>';
|
||||||
|
|
||||||
|
dom.innerHTML = toolbarHTML;
|
||||||
|
|
||||||
|
// Attach event listeners for diff navigation
|
||||||
|
if (hasDiff) {
|
||||||
const prevBtn = dom.querySelector('.cm-diff-nav-prev');
|
const prevBtn = dom.querySelector('.cm-diff-nav-prev');
|
||||||
const nextBtn = dom.querySelector('.cm-diff-nav-next');
|
const nextBtn = dom.querySelector('.cm-diff-nav-next');
|
||||||
|
|
||||||
@@ -128,10 +194,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
if (chunks.length === 0) return;
|
if (chunks.length === 0) return;
|
||||||
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
|
currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1;
|
||||||
|
|
||||||
// Navigate to the chunk - use fromB which is the position in the current document
|
|
||||||
const chunk = chunks[currentIndex];
|
const chunk = chunks[currentIndex];
|
||||||
if (chunk) {
|
if (chunk) {
|
||||||
// Scroll to the start of the chunk in the B side (current document)
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
||||||
});
|
});
|
||||||
@@ -143,16 +207,39 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
if (chunks.length === 0) return;
|
if (chunks.length === 0) return;
|
||||||
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
|
currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0;
|
||||||
|
|
||||||
// Navigate to the chunk - use fromB which is the position in the current document
|
|
||||||
const chunk = chunks[currentIndex];
|
const chunk = chunks[currentIndex];
|
||||||
if (chunk) {
|
if (chunk) {
|
||||||
// Scroll to the start of the chunk in the B side (current document)
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
updatePanel();
|
updatePanel();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach event listener for toggle diff button
|
||||||
|
if (file.diffInfo) {
|
||||||
|
const toggleDiffBtn = dom.querySelector('.cm-toggle-diff-btn');
|
||||||
|
toggleDiffBtn?.addEventListener('click', () => {
|
||||||
|
setShowDiff(!showDiff);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach event listener for settings button
|
||||||
|
const settingsBtn = dom.querySelector('.cm-settings-btn');
|
||||||
|
settingsBtn?.addEventListener('click', () => {
|
||||||
|
if (window.openSettings) {
|
||||||
|
window.openSettings('appearance');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach event listener for expand button
|
||||||
|
if (isSidebar && onToggleExpand) {
|
||||||
|
const expandBtn = dom.querySelector('.cm-expand-btn');
|
||||||
|
expandBtn?.addEventListener('click', () => {
|
||||||
|
onToggleExpand();
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePanel();
|
updatePanel();
|
||||||
@@ -165,7 +252,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [showPanel.of(createPanel)];
|
return [showPanel.of(createPanel)];
|
||||||
}, [file.diffInfo, showDiff]);
|
}, [file.diffInfo, showDiff, isSidebar, isExpanded, onToggleExpand]);
|
||||||
|
|
||||||
// Get language extension based on file extension
|
// Get language extension based on file extension
|
||||||
const getLanguageExtension = (filename) => {
|
const getLanguageExtension = (filename) => {
|
||||||
@@ -290,6 +377,57 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
setIsFullscreen(!isFullscreen);
|
setIsFullscreen(!isFullscreen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save theme preference to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorTheme', isDarkMode ? 'dark' : 'light');
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
// Save word wrap preference to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorWordWrap', wordWrap.toString());
|
||||||
|
}, [wordWrap]);
|
||||||
|
|
||||||
|
// Listen for settings changes from the Settings modal
|
||||||
|
useEffect(() => {
|
||||||
|
const handleStorageChange = () => {
|
||||||
|
const newTheme = localStorage.getItem('codeEditorTheme');
|
||||||
|
if (newTheme) {
|
||||||
|
setIsDarkMode(newTheme === 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWordWrap = localStorage.getItem('codeEditorWordWrap');
|
||||||
|
if (newWordWrap !== null) {
|
||||||
|
setWordWrap(newWordWrap === 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newShowMinimap = localStorage.getItem('codeEditorShowMinimap');
|
||||||
|
if (newShowMinimap !== null) {
|
||||||
|
setMinimapEnabled(newShowMinimap !== 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newShowLineNumbers = localStorage.getItem('codeEditorLineNumbers');
|
||||||
|
if (newShowLineNumbers !== null) {
|
||||||
|
setShowLineNumbers(newShowLineNumbers !== 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFontSize = localStorage.getItem('codeEditorFontSize');
|
||||||
|
if (newFontSize) {
|
||||||
|
setFontSize(newFontSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for storage events (changes from other tabs/windows)
|
||||||
|
window.addEventListener('storage', handleStorageChange);
|
||||||
|
|
||||||
|
// Custom event for same-window updates
|
||||||
|
window.addEventListener('codeEditorSettingsChanged', handleStorageChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', handleStorageChange);
|
||||||
|
window.removeEventListener('codeEditorSettingsChanged', handleStorageChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle keyboard shortcuts
|
// Handle keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = (e) => {
|
||||||
@@ -321,7 +459,15 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
<div className="fixed inset-0 z-50 md:bg-black/50 md:flex md:items-center md:justify-center">
|
{isSidebar ? (
|
||||||
|
<div className="w-full h-full flex items-center justify-center bg-white dark:bg-gray-900">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||||
|
<span className="text-gray-900 dark:text-white">Loading {file.name}...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="fixed inset-0 z-40 md:bg-black/50 md:flex md:items-center md:justify-center">
|
||||||
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
|
<div className="code-editor-loading w-full h-full md:rounded-lg md:w-auto md:h-auto p-8 flex items-center justify-center">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||||
@@ -329,6 +475,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -372,8 +519,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
|
background-color: ${isDarkMode ? '#1e1e1e' : '#f5f5f5'};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Diff navigation panel styling */
|
/* Editor toolbar panel styling */
|
||||||
.cm-diff-navigation-panel {
|
.cm-editor-toolbar-panel {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
|
background-color: ${isDarkMode ? '#1f2937' : '#ffffff'};
|
||||||
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
|
border-bottom: 1px solid ${isDarkMode ? '#374151' : '#e5e7eb'};
|
||||||
@@ -381,7 +528,8 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-diff-nav-btn {
|
.cm-diff-nav-btn,
|
||||||
|
.cm-toolbar-btn {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -391,9 +539,11 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-diff-nav-btn:hover {
|
.cm-diff-nav-btn:hover,
|
||||||
|
.cm-toolbar-btn:hover {
|
||||||
background-color: ${isDarkMode ? '#374151' : '#f3f4f6'};
|
background-color: ${isDarkMode ? '#374151' : '#f3f4f6'};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,67 +553,36 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
<div className={`fixed inset-0 z-50 ${
|
<div className={isSidebar ?
|
||||||
|
'w-full h-full flex flex-col' :
|
||||||
|
`fixed inset-0 z-40 ${
|
||||||
// Mobile: native fullscreen, Desktop: modal with backdrop
|
// Mobile: native fullscreen, Desktop: modal with backdrop
|
||||||
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
|
'md:bg-black/50 md:flex md:items-center md:justify-center md:p-4'
|
||||||
} ${isFullscreen ? 'md:p-0' : ''}`}>
|
} ${isFullscreen ? 'md:p-0' : ''}`}>
|
||||||
<div className={`bg-white shadow-2xl flex flex-col ${
|
<div className={isSidebar ?
|
||||||
|
'bg-white dark:bg-gray-900 flex flex-col w-full h-full' :
|
||||||
|
`bg-white shadow-2xl flex flex-col ${
|
||||||
// Mobile: always fullscreen, Desktop: modal sizing
|
// Mobile: always fullscreen, Desktop: modal sizing
|
||||||
'w-full h-full md:rounded-lg md:shadow-2xl' +
|
'w-full h-full md:rounded-lg md:shadow-2xl' +
|
||||||
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
|
(isFullscreen ? ' md:w-full md:h-full md:rounded-none' : ' md:w-full md:max-w-6xl md:h-[80vh] md:max-h-[80vh]')
|
||||||
}`}>
|
}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 flex-shrink-0 min-w-0">
|
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 min-w-0">
|
||||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center flex-shrink-0">
|
|
||||||
<span className="text-white text-sm font-mono">
|
|
||||||
{file.name.split('.').pop()?.toUpperCase() || 'FILE'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<h3 className="font-medium text-gray-900 truncate">{file.name}</h3>
|
<h3 className="font-medium text-gray-900 dark:text-white truncate">{file.name}</h3>
|
||||||
{file.diffInfo && (
|
{file.diffInfo && (
|
||||||
<span className="text-xs bg-blue-100 text-blue-600 px-2 py-1 rounded whitespace-nowrap">
|
<span className="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-2 py-1 rounded whitespace-nowrap">
|
||||||
📝 Has changes
|
Showing changes
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500 truncate">{file.path}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400 truncate">{file.path}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
|
<div className="flex items-center gap-1 md:gap-2 flex-shrink-0">
|
||||||
{file.diffInfo && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDiff(!showDiff)}
|
|
||||||
className="p-2 md:p-2 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-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
|
||||||
title={showDiff ? "Hide diff highlighting" : "Show diff highlighting"}
|
|
||||||
>
|
|
||||||
{showDiff ? <EyeOff className="w-5 h-5 md:w-4 md:h-4" /> : <Eye className="w-5 h-5 md:w-4 md:h-4" />}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setWordWrap(!wordWrap)}
|
|
||||||
className={`p-2 md:p-2 rounded-md hover:bg-gray-100 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center ${
|
|
||||||
wordWrap
|
|
||||||
? 'text-blue-600 bg-blue-50'
|
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
|
||||||
}`}
|
|
||||||
title={wordWrap ? 'Disable word wrap' : 'Enable word wrap'}
|
|
||||||
>
|
|
||||||
<span className="text-sm md:text-xs font-mono font-bold">↵</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setIsDarkMode(!isDarkMode)}
|
|
||||||
className="p-2 md:p-2 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-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
|
||||||
title="Toggle theme"
|
|
||||||
>
|
|
||||||
<span className="text-lg md:text-base">{isDarkMode ? '☀️' : '🌙'}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
className="p-2 md:p-2 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-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
className="p-2 md:p-2 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-800 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center"
|
||||||
@@ -496,6 +615,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{!isSidebar && (
|
||||||
<button
|
<button
|
||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
className="hidden md:flex p-2 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-800 items-center justify-center"
|
className="hidden md:flex p-2 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-800 items-center justify-center"
|
||||||
@@ -503,6 +623,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
>
|
>
|
||||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -522,6 +643,9 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
onChange={setContent}
|
onChange={setContent}
|
||||||
extensions={[
|
extensions={[
|
||||||
...getLanguageExtension(file.name),
|
...getLanguageExtension(file.name),
|
||||||
|
// Always show the toolbar
|
||||||
|
...editorToolbarPanel,
|
||||||
|
// Only show diff-related extensions when diff is enabled
|
||||||
...(file.diffInfo && showDiff && file.diffInfo.old_string !== undefined
|
...(file.diffInfo && showDiff && file.diffInfo.old_string !== undefined
|
||||||
? [
|
? [
|
||||||
unifiedMergeView({
|
unifiedMergeView({
|
||||||
@@ -533,8 +657,7 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
// NOTE: NO collapseUnchanged - this shows the full file!
|
// NOTE: NO collapseUnchanged - this shows the full file!
|
||||||
}),
|
}),
|
||||||
...minimapExtension,
|
...minimapExtension,
|
||||||
...scrollToFirstChunkExtension,
|
...scrollToFirstChunkExtension
|
||||||
...diffNavigationPanel
|
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(wordWrap ? [EditorView.lineWrapping] : [])
|
...(wordWrap ? [EditorView.lineWrapping] : [])
|
||||||
@@ -542,11 +665,11 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
theme={isDarkMode ? oneDark : undefined}
|
theme={isDarkMode ? oneDark : undefined}
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '14px',
|
fontSize: `${fontSize}px`,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
basicSetup={{
|
basicSetup={{
|
||||||
lineNumbers: true,
|
lineNumbers: showLineNumbers,
|
||||||
foldGutter: true,
|
foldGutter: true,
|
||||||
dropCursor: false,
|
dropCursor: false,
|
||||||
allowMultipleSelections: false,
|
allowMultipleSelections: false,
|
||||||
@@ -565,7 +688,6 @@ function CodeEditor({ file, onClose, projectPath }) {
|
|||||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<span>Lines: {content.split('\n').length}</span>
|
<span>Lines: {content.split('\n').length}</span>
|
||||||
<span>Characters: {content.length}</span>
|
<span>Characters: {content.length}</span>
|
||||||
<span>Language: {file.name.split('.').pop()?.toUpperCase() || 'Text'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* No session protection logic is implemented here - it's purely a props bridge.
|
* No session protection logic is implemented here - it's purely a props bridge.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import ChatInterface from './ChatInterface';
|
import ChatInterface from './ChatInterface';
|
||||||
import FileTree from './FileTree';
|
import FileTree from './FileTree';
|
||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from './CodeEditor';
|
||||||
@@ -61,6 +61,10 @@ function MainContent({
|
|||||||
const [editingFile, setEditingFile] = useState(null);
|
const [editingFile, setEditingFile] = useState(null);
|
||||||
const [selectedTask, setSelectedTask] = useState(null);
|
const [selectedTask, setSelectedTask] = useState(null);
|
||||||
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
const [showTaskDetail, setShowTaskDetail] = useState(false);
|
||||||
|
const [editorWidth, setEditorWidth] = useState(600);
|
||||||
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
|
const [editorExpanded, setEditorExpanded] = useState(false);
|
||||||
|
const resizeRef = useRef(null);
|
||||||
|
|
||||||
// PRD Editor state
|
// PRD Editor state
|
||||||
const [showPRDEditor, setShowPRDEditor] = useState(false);
|
const [showPRDEditor, setShowPRDEditor] = useState(false);
|
||||||
@@ -127,6 +131,11 @@ function MainContent({
|
|||||||
|
|
||||||
const handleCloseEditor = () => {
|
const handleCloseEditor = () => {
|
||||||
setEditingFile(null);
|
setEditingFile(null);
|
||||||
|
setEditorExpanded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleEditorExpand = () => {
|
||||||
|
setEditorExpanded(!editorExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTaskClick = (task) => {
|
const handleTaskClick = (task) => {
|
||||||
@@ -153,6 +162,52 @@ function MainContent({
|
|||||||
console.log('Update task status:', taskId, newStatus);
|
console.log('Update task status:', taskId, newStatus);
|
||||||
refreshTasks?.();
|
refreshTasks?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle resize functionality
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
if (isMobile) return; // Disable resize on mobile
|
||||||
|
setIsResizing(true);
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const container = resizeRef.current?.parentElement;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const newWidth = containerRect.right - e.clientX;
|
||||||
|
|
||||||
|
// Min width: 300px, Max width: 80% of container
|
||||||
|
const minWidth = 300;
|
||||||
|
const maxWidth = containerRect.width * 0.8;
|
||||||
|
|
||||||
|
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||||
|
setEditorWidth(newWidth);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsResizing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isResizing) {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
}, [isResizing]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
@@ -237,7 +292,7 @@ function MainContent({
|
|||||||
<div
|
<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"
|
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 justify-between relative">
|
||||||
<div className="flex items-center space-x-2 sm:space-x-3">
|
<div className="flex items-center space-x-2 sm:space-x-3">
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<button
|
<button
|
||||||
@@ -409,8 +464,10 @@ function MainContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area with Right Sidebar */}
|
||||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
<div className="flex-1 flex min-h-0 overflow-hidden">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className={`flex-1 flex flex-col min-h-0 overflow-hidden ${editingFile ? 'mr-0' : ''} ${editorExpanded ? 'hidden' : ''}`}>
|
||||||
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
||||||
<ErrorBoundary showDetails={true}>
|
<ErrorBoundary showDetails={true}>
|
||||||
<ChatInterface
|
<ChatInterface
|
||||||
@@ -515,12 +572,47 @@ function MainContent({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Code Editor Modal */}
|
{/* Code Editor Right Sidebar - Desktop only, Mobile uses modal */}
|
||||||
{editingFile && (
|
{editingFile && !isMobile && (
|
||||||
|
<>
|
||||||
|
{/* Resize Handle - Hidden when expanded */}
|
||||||
|
{!editorExpanded && (
|
||||||
|
<div
|
||||||
|
ref={resizeRef}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
className="flex-shrink-0 w-1 bg-gray-200 dark:bg-gray-700 hover:bg-blue-500 dark:hover:bg-blue-600 cursor-col-resize transition-colors relative group"
|
||||||
|
title="Drag to resize"
|
||||||
|
>
|
||||||
|
{/* Visual indicator on hover */}
|
||||||
|
<div className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-1 bg-blue-500 dark:bg-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Editor Sidebar */}
|
||||||
|
<div
|
||||||
|
className={`flex-shrink-0 border-l border-gray-200 dark:border-gray-700 h-full overflow-hidden ${editorExpanded ? 'flex-1' : ''}`}
|
||||||
|
style={editorExpanded ? {} : { width: `${editorWidth}px` }}
|
||||||
|
>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
file={editingFile}
|
file={editingFile}
|
||||||
onClose={handleCloseEditor}
|
onClose={handleCloseEditor}
|
||||||
projectPath={selectedProject?.path}
|
projectPath={selectedProject?.path}
|
||||||
|
isSidebar={true}
|
||||||
|
isExpanded={editorExpanded}
|
||||||
|
onToggleExpand={handleToggleEditorExpand}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Code Editor Modal for Mobile */}
|
||||||
|
{editingFile && isMobile && (
|
||||||
|
<CodeEditor
|
||||||
|
file={editingFile}
|
||||||
|
onClose={handleCloseEditor}
|
||||||
|
projectPath={selectedProject?.path}
|
||||||
|
isSidebar={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const QuickSettingsPanel = ({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={autoExpandTools}
|
checked={autoExpandTools}
|
||||||
onChange={(e) => onAutoExpandChange(e.target.checked)}
|
onChange={(e) => onAutoExpandChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ const QuickSettingsPanel = ({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showRawParameters}
|
checked={showRawParameters}
|
||||||
onChange={(e) => onShowRawParametersChange(e.target.checked)}
|
onChange={(e) => onShowRawParametersChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ const QuickSettingsPanel = ({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showThinking}
|
checked={showThinking}
|
||||||
onChange={(e) => onShowThinkingChange(e.target.checked)}
|
onChange={(e) => onShowThinkingChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +155,7 @@ const QuickSettingsPanel = ({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={autoScrollToBottom}
|
checked={autoScrollToBottom}
|
||||||
onChange={(e) => onAutoScrollChange(e.target.checked)}
|
onChange={(e) => onAutoScrollChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +173,7 @@ const QuickSettingsPanel = ({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={sendByCtrlEnter}
|
checked={sendByCtrlEnter}
|
||||||
onChange={(e) => onSendByCtrlEnterChange(e.target.checked)}
|
onChange={(e) => onSendByCtrlEnterChange(e.target.checked)}
|
||||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600"
|
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 focus:ring-2 dark:focus:ring-blue-400 bg-gray-100 dark:bg-gray-800 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
|
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ClaudeLogo from './ClaudeLogo';
|
|||||||
import CursorLogo from './CursorLogo';
|
import CursorLogo from './CursorLogo';
|
||||||
import CredentialsSettings from './CredentialsSettings';
|
import CredentialsSettings from './CredentialsSettings';
|
||||||
|
|
||||||
function Settings({ isOpen, onClose, projects = [] }) {
|
function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
|
||||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
const { isDarkMode, toggleDarkMode } = useTheme();
|
||||||
const {
|
const {
|
||||||
tasksEnabled,
|
tasksEnabled,
|
||||||
@@ -52,10 +52,27 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
const [mcpTestResults, setMcpTestResults] = useState({});
|
const [mcpTestResults, setMcpTestResults] = useState({});
|
||||||
const [mcpServerTools, setMcpServerTools] = useState({});
|
const [mcpServerTools, setMcpServerTools] = useState({});
|
||||||
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
|
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
|
||||||
const [activeTab, setActiveTab] = useState('tools');
|
const [activeTab, setActiveTab] = useState(initialTab);
|
||||||
const [jsonValidationError, setJsonValidationError] = useState('');
|
const [jsonValidationError, setJsonValidationError] = useState('');
|
||||||
const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor'
|
const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor'
|
||||||
|
|
||||||
|
// Code Editor settings
|
||||||
|
const [codeEditorTheme, setCodeEditorTheme] = useState(() =>
|
||||||
|
localStorage.getItem('codeEditorTheme') || 'dark'
|
||||||
|
);
|
||||||
|
const [codeEditorWordWrap, setCodeEditorWordWrap] = useState(() =>
|
||||||
|
localStorage.getItem('codeEditorWordWrap') === 'true'
|
||||||
|
);
|
||||||
|
const [codeEditorShowMinimap, setCodeEditorShowMinimap] = useState(() =>
|
||||||
|
localStorage.getItem('codeEditorShowMinimap') !== 'false' // Default true
|
||||||
|
);
|
||||||
|
const [codeEditorLineNumbers, setCodeEditorLineNumbers] = useState(() =>
|
||||||
|
localStorage.getItem('codeEditorLineNumbers') !== 'false' // Default true
|
||||||
|
);
|
||||||
|
const [codeEditorFontSize, setCodeEditorFontSize] = useState(() =>
|
||||||
|
localStorage.getItem('codeEditorFontSize') || '14'
|
||||||
|
);
|
||||||
|
|
||||||
// Cursor-specific states
|
// Cursor-specific states
|
||||||
const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
|
const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
|
||||||
const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]);
|
const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]);
|
||||||
@@ -327,8 +344,36 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
// Set the active tab when the modal opens
|
||||||
|
setActiveTab(initialTab);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen, initialTab]);
|
||||||
|
|
||||||
|
// Persist code editor settings to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorTheme', codeEditorTheme);
|
||||||
|
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||||
|
}, [codeEditorTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorWordWrap', codeEditorWordWrap.toString());
|
||||||
|
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||||
|
}, [codeEditorWordWrap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorShowMinimap', codeEditorShowMinimap.toString());
|
||||||
|
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||||
|
}, [codeEditorShowMinimap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorLineNumbers', codeEditorLineNumbers.toString());
|
||||||
|
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||||
|
}, [codeEditorLineNumbers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('codeEditorFontSize', codeEditorFontSize);
|
||||||
|
window.dispatchEvent(new Event('codeEditorSettingsChanged'));
|
||||||
|
}, [codeEditorFontSize]);
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -625,7 +670,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-background/95">
|
<div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[9999] md:p-4 bg-background/95">
|
||||||
<div className="bg-background border border-border md:rounded-lg shadow-xl w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col">
|
<div className="bg-background border border-border md:rounded-lg shadow-xl w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col">
|
||||||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0">
|
<div className="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -758,6 +803,158 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Code Editor Settings */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">Code Editor</h3>
|
||||||
|
|
||||||
|
{/* Editor Theme */}
|
||||||
|
<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">
|
||||||
|
Editor Theme
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Default theme for the code editor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCodeEditorTheme(codeEditorTheme === 'dark' ? 'light' : 'dark')}
|
||||||
|
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={codeEditorTheme === 'dark'}
|
||||||
|
aria-label="Toggle editor theme"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Toggle editor theme</span>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
codeEditorTheme === 'dark' ? '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`}
|
||||||
|
>
|
||||||
|
{codeEditorTheme === 'dark' ? (
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Word Wrap */}
|
||||||
|
<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">
|
||||||
|
Word Wrap
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Enable word wrapping by default in the editor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCodeEditorWordWrap(!codeEditorWordWrap)}
|
||||||
|
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={codeEditorWordWrap}
|
||||||
|
aria-label="Toggle word wrap"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Toggle word wrap</span>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
codeEditorWordWrap ? 'translate-x-7' : 'translate-x-1'
|
||||||
|
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show Minimap */}
|
||||||
|
<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">
|
||||||
|
Show Minimap
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Display a minimap for easier navigation in diff view
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCodeEditorShowMinimap(!codeEditorShowMinimap)}
|
||||||
|
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={codeEditorShowMinimap}
|
||||||
|
aria-label="Toggle minimap"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Toggle minimap</span>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
codeEditorShowMinimap ? 'translate-x-7' : 'translate-x-1'
|
||||||
|
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show Line Numbers */}
|
||||||
|
<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">
|
||||||
|
Show Line Numbers
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Display line numbers in the editor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCodeEditorLineNumbers(!codeEditorLineNumbers)}
|
||||||
|
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={codeEditorLineNumbers}
|
||||||
|
aria-label="Toggle line numbers"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Toggle line numbers</span>
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
codeEditorLineNumbers ? 'translate-x-7' : 'translate-x-1'
|
||||||
|
} inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Font Size */}
|
||||||
|
<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">
|
||||||
|
Font Size
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Editor font size in pixels
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={codeEditorFontSize}
|
||||||
|
onChange={(e) => setCodeEditorFontSize(e.target.value)}
|
||||||
|
className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-24"
|
||||||
|
>
|
||||||
|
<option value="10">10px</option>
|
||||||
|
<option value="11">11px</option>
|
||||||
|
<option value="12">12px</option>
|
||||||
|
<option value="13">13px</option>
|
||||||
|
<option value="14">14px</option>
|
||||||
|
<option value="15">15px</option>
|
||||||
|
<option value="16">16px</option>
|
||||||
|
<option value="18">18px</option>
|
||||||
|
<option value="20">20px</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -818,7 +1015,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={skipPermissions}
|
checked={skipPermissions}
|
||||||
onChange={(e) => setSkipPermissions(e.target.checked)}
|
onChange={(e) => setSkipPermissions(e.target.checked)}
|
||||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||||
@@ -1578,7 +1775,7 @@ function Settings({ isOpen, onClose, projects = [] }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={cursorSkipPermissions}
|
checked={cursorSkipPermissions}
|
||||||
onChange={(e) => setCursorSkipPermissions(e.target.checked)}
|
onChange={(e) => setCursorSkipPermissions(e.target.checked)}
|
||||||
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2 checked:bg-blue-600 dark:checked:bg-blue-600"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-orange-900 dark:text-orange-100">
|
<div className="font-medium text-orange-900 dark:text-orange-100">
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ function Sidebar({
|
|||||||
updateAvailable,
|
updateAvailable,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
|
releaseInfo,
|
||||||
onShowVersionModal,
|
onShowVersionModal,
|
||||||
isPWA,
|
isPWA,
|
||||||
isMobile
|
isMobile,
|
||||||
|
onToggleSidebar
|
||||||
}) {
|
}) {
|
||||||
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
||||||
const [editingProject, setEditingProject] = useState(null);
|
const [editingProject, setEditingProject] = useState(null);
|
||||||
@@ -586,34 +588,24 @@ function Sidebar({
|
|||||||
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
{onToggleSidebar && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-9 w-9 px-0 hover:bg-accent transition-colors duration-200 group"
|
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200"
|
||||||
onClick={async () => {
|
onClick={onToggleSidebar}
|
||||||
setIsRefreshing(true);
|
title="Hide sidebar"
|
||||||
try {
|
|
||||||
await onRefresh();
|
|
||||||
} finally {
|
|
||||||
setIsRefreshing(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isRefreshing}
|
|
||||||
title="Refresh projects and sessions (Ctrl+R)"
|
|
||||||
>
|
>
|
||||||
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
<svg
|
||||||
</Button>
|
className="w-4 h-4"
|
||||||
<Button
|
fill="none"
|
||||||
variant="default"
|
stroke="currentColor"
|
||||||
size="sm"
|
viewBox="0 0 24 24"
|
||||||
className="h-9 w-9 px-0 bg-primary hover:bg-primary/90 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
||||||
onClick={() => setShowNewProject(true)}
|
|
||||||
title="Create new project (Ctrl+N)"
|
|
||||||
>
|
>
|
||||||
<FolderPlus className="w-4 h-4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Header */}
|
{/* Mobile Header */}
|
||||||
@@ -913,9 +905,9 @@ function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Search Filter */}
|
{/* Search Filter and Actions */}
|
||||||
{projects.length > 0 && !isLoading && (
|
{projects.length > 0 && !isLoading && (
|
||||||
<div className="px-3 md:px-4 py-2 border-b border-border">
|
<div className="px-3 md:px-4 py-2 border-b border-border space-y-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -934,6 +926,39 @@ function Sidebar({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons - Desktop only */}
|
||||||
|
{!isMobile && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90 transition-all duration-200"
|
||||||
|
onClick={() => setShowNewProject(true)}
|
||||||
|
title="Create new project (Ctrl+N)"
|
||||||
|
>
|
||||||
|
<FolderPlus className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
New Project
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 px-0 hover:bg-accent transition-colors duration-200 group"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
await onRefresh();
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
title="Refresh projects and sessions (Ctrl+R)"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3.5 h-3.5 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1611,8 +1636,10 @@ function Sidebar({
|
|||||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">Update Available</div>
|
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||||
<div className="text-xs text-blue-600 dark:text-blue-400">Version {latestVersion} is ready</div>
|
{releaseInfo?.title || `Version ${latestVersion}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-600 dark:text-blue-400">Update available</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1630,8 +1657,10 @@ function Sidebar({
|
|||||||
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 text-left">
|
<div className="min-w-0 flex-1 text-left">
|
||||||
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">Update Available</div>
|
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||||
<div className="text-xs text-blue-600 dark:text-blue-400">Version {latestVersion} is ready</div>
|
{releaseInfo?.title || `Version ${latestVersion}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-600 dark:text-blue-400">Update available</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { version } from '../../package.json';
|
|||||||
export const useVersionCheck = (owner, repo) => {
|
export const useVersionCheck = (owner, repo) => {
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false);
|
const [updateAvailable, setUpdateAvailable] = useState(false);
|
||||||
const [latestVersion, setLatestVersion] = useState(null);
|
const [latestVersion, setLatestVersion] = useState(null);
|
||||||
|
const [releaseInfo, setReleaseInfo] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkVersion = async () => {
|
const checkVersion = async () => {
|
||||||
@@ -17,16 +18,26 @@ export const useVersionCheck = (owner, repo) => {
|
|||||||
const latest = data.tag_name.replace(/^v/, '');
|
const latest = data.tag_name.replace(/^v/, '');
|
||||||
setLatestVersion(latest);
|
setLatestVersion(latest);
|
||||||
setUpdateAvailable(version !== latest);
|
setUpdateAvailable(version !== latest);
|
||||||
|
|
||||||
|
// Store release information
|
||||||
|
setReleaseInfo({
|
||||||
|
title: data.name || data.tag_name,
|
||||||
|
body: data.body || '',
|
||||||
|
htmlUrl: data.html_url || `https://github.com/${owner}/${repo}/releases/latest`,
|
||||||
|
publishedAt: data.published_at
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// No releases found, don't show update notification
|
// No releases found, don't show update notification
|
||||||
setUpdateAvailable(false);
|
setUpdateAvailable(false);
|
||||||
setLatestVersion(null);
|
setLatestVersion(null);
|
||||||
|
setReleaseInfo(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Version check failed:', error);
|
console.error('Version check failed:', error);
|
||||||
// On error, don't show update notification
|
// On error, don't show update notification
|
||||||
setUpdateAvailable(false);
|
setUpdateAvailable(false);
|
||||||
setLatestVersion(null);
|
setLatestVersion(null);
|
||||||
|
setReleaseInfo(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,5 +46,5 @@ export const useVersionCheck = (owner, repo) => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [owner, repo]);
|
}, [owner, repo]);
|
||||||
|
|
||||||
return { updateAvailable, latestVersion, currentVersion: version };
|
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo };
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user