mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-12 13:19:43 +00:00
UX enhancements on gitpanel and Shell to make them more mobile friendly
This commit is contained in:
@@ -583,4 +583,43 @@ router.post('/pull', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Discard changes for a specific file
|
||||||
|
router.post('/discard', async (req, res) => {
|
||||||
|
const { project, file } = req.body;
|
||||||
|
|
||||||
|
if (!project || !file) {
|
||||||
|
return res.status(400).json({ error: 'Project name and file path are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectPath = await getActualProjectPath(project);
|
||||||
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
|
// Check file status to determine correct discard command
|
||||||
|
const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
|
||||||
|
|
||||||
|
if (!statusOutput.trim()) {
|
||||||
|
return res.status(400).json({ error: 'No changes to discard for this file' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = statusOutput.substring(0, 2);
|
||||||
|
|
||||||
|
if (status === '??') {
|
||||||
|
// Untracked file - delete it
|
||||||
|
await fs.unlink(path.join(projectPath, file));
|
||||||
|
} else if (status.includes('M') || status.includes('D')) {
|
||||||
|
// Modified or deleted file - restore from HEAD
|
||||||
|
await execAsync(`git restore "${file}"`, { cwd: projectPath });
|
||||||
|
} else if (status.includes('A')) {
|
||||||
|
// Added file - unstage it
|
||||||
|
await execAsync(`git reset HEAD "${file}"`, { cwd: projectPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, message: `Changes discarded for ${file}` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Git discard error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1027,6 +1027,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
|
||||||
const [selectedCommandIndex, setSelectedCommandIndex] = useState(-1);
|
const [selectedCommandIndex, setSelectedCommandIndex] = useState(-1);
|
||||||
const [slashPosition, setSlashPosition] = useState(-1);
|
const [slashPosition, setSlashPosition] = useState(-1);
|
||||||
|
const [visibleMessageCount, setVisibleMessageCount] = useState(100);
|
||||||
const [claudeStatus, setClaudeStatus] = useState(null);
|
const [claudeStatus, setClaudeStatus] = useState(null);
|
||||||
|
|
||||||
|
|
||||||
@@ -1630,14 +1631,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [input]);
|
}, [input]);
|
||||||
|
|
||||||
// Show only recent messages for better performance (last 100 messages)
|
// Show only recent messages for better performance
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
const maxMessages = 100;
|
if (chatMessages.length <= visibleMessageCount) {
|
||||||
if (chatMessages.length <= maxMessages) {
|
|
||||||
return chatMessages;
|
return chatMessages;
|
||||||
}
|
}
|
||||||
return chatMessages.slice(-maxMessages);
|
return chatMessages.slice(-visibleMessageCount);
|
||||||
}, [chatMessages]);
|
}, [chatMessages, visibleMessageCount]);
|
||||||
|
|
||||||
// Capture scroll position before render when auto-scroll is disabled
|
// Capture scroll position before render when auto-scroll is disabled
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1737,6 +1737,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load earlier messages by increasing the visible message count
|
||||||
|
const loadEarlierMessages = useCallback(() => {
|
||||||
|
setVisibleMessageCount(prevCount => prevCount + 100);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle image files from drag & drop or file picker
|
// Handle image files from drag & drop or file picker
|
||||||
const handleImageFiles = useCallback((files) => {
|
const handleImageFiles = useCallback((files) => {
|
||||||
const validFiles = files.filter(file => {
|
const validFiles = files.filter(file => {
|
||||||
@@ -2081,10 +2086,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{chatMessages.length > 100 && (
|
{chatMessages.length > visibleMessageCount && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
|
<div className="text-center text-gray-500 dark:text-gray-400 text-sm py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
Showing last 100 messages ({chatMessages.length} total) •
|
Showing last {visibleMessageCount} messages ({chatMessages.length} total) •
|
||||||
<button className="ml-1 text-blue-600 hover:text-blue-700 underline">
|
<button
|
||||||
|
className="ml-1 text-blue-600 hover:text-blue-700 underline"
|
||||||
|
onClick={loadEarlierMessages}
|
||||||
|
>
|
||||||
Load earlier messages
|
Load earlier messages
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download } from 'lucide-react';
|
import { GitBranch, GitCommit, Plus, Minus, RefreshCw, Check, X, ChevronDown, ChevronRight, Info, History, FileText, Mic, MicOff, Sparkles, Download, RotateCcw, Trash2, AlertTriangle } from 'lucide-react';
|
||||||
import { MicButton } from './MicButton.jsx';
|
import { MicButton } from './MicButton.jsx';
|
||||||
import { authenticatedFetch } from '../utils/api';
|
import { authenticatedFetch } from '../utils/api';
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
const [isPulling, setIsPulling] = useState(false);
|
const [isPulling, setIsPulling] = useState(false);
|
||||||
const [isCommitAreaCollapsed, setIsCommitAreaCollapsed] = useState(isMobile); // Collapsed by default on mobile
|
const [isCommitAreaCollapsed, setIsCommitAreaCollapsed] = useState(isMobile); // Collapsed by default on mobile
|
||||||
|
const [confirmAction, setConfirmAction] = useState(null); // { type: 'discard|commit|pull', file?: string, message?: string }
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
|
|
||||||
@@ -237,6 +238,57 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const discardChanges = async (filePath) => {
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch('/api/git/discard', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
project: selectedProject.name,
|
||||||
|
file: filePath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
// Remove from selected files and refresh status
|
||||||
|
setSelectedFiles(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(filePath);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
fetchGitStatus();
|
||||||
|
} else {
|
||||||
|
console.error('Discard failed:', data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error discarding changes:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmAndExecute = async () => {
|
||||||
|
if (!confirmAction) return;
|
||||||
|
|
||||||
|
const { type, file, message } = confirmAction;
|
||||||
|
setConfirmAction(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'discard':
|
||||||
|
await discardChanges(file);
|
||||||
|
break;
|
||||||
|
case 'commit':
|
||||||
|
await handleCommit();
|
||||||
|
break;
|
||||||
|
case 'pull':
|
||||||
|
await handlePull();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error executing ${type}:`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchFileDiff = async (filePath) => {
|
const fetchFileDiff = async (filePath) => {
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
|
const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`);
|
||||||
@@ -476,6 +528,24 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<ChevronRight className={`w-3 h-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} />
|
<ChevronRight className={`w-3 h-3 transition-transform duration-200 ease-in-out ${isExpanded ? 'rotate-90' : 'rotate-0'}`} />
|
||||||
</div>
|
</div>
|
||||||
<span className={`flex-1 truncate ${isMobile ? 'text-xs' : 'text-sm'}`}>{filePath}</span>
|
<span className={`flex-1 truncate ${isMobile ? 'text-xs' : 'text-sm'}`}>{filePath}</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{(status === 'M' || status === 'D') && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setConfirmAction({
|
||||||
|
type: 'discard',
|
||||||
|
file: filePath,
|
||||||
|
message: `Discard all changes to "${filePath}"? This action cannot be undone.`
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`${isMobile ? 'px-2 py-1 text-xs' : 'p-1'} hover:bg-red-100 dark:hover:bg-red-900 rounded text-red-600 dark:text-red-400 font-medium flex items-center gap-1`}
|
||||||
|
title="Discard changes"
|
||||||
|
>
|
||||||
|
<Trash2 className={`${isMobile ? 'w-3 h-3' : 'w-3 h-3'}`} />
|
||||||
|
{isMobile && <span>Discard</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
className={`inline-flex items-center justify-center w-5 h-5 rounded text-xs font-bold border ${
|
||||||
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
status === 'M' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300 border-yellow-200 dark:border-yellow-800' :
|
||||||
@@ -489,6 +559,7 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className={`bg-gray-50 dark:bg-gray-900 transition-all duration-400 ease-in-out overflow-hidden ${
|
<div className={`bg-gray-50 dark:bg-gray-900 transition-all duration-400 ease-in-out overflow-hidden ${
|
||||||
isExpanded && diff
|
isExpanded && diff
|
||||||
? 'max-h-[600px] opacity-100 translate-y-0'
|
? 'max-h-[600px] opacity-100 translate-y-0'
|
||||||
@@ -618,7 +689,10 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
{/* Pull button - show when behind (primary action) */}
|
{/* Pull button - show when behind (primary action) */}
|
||||||
{remoteStatus.behind > 0 && (
|
{remoteStatus.behind > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={handlePull}
|
onClick={() => setConfirmAction({
|
||||||
|
type: 'pull',
|
||||||
|
message: `Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}?`
|
||||||
|
})}
|
||||||
disabled={isPulling}
|
disabled={isPulling}
|
||||||
className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center gap-1"
|
className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center gap-1"
|
||||||
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
||||||
@@ -785,7 +859,10 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
{selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={handleCommit}
|
onClick={() => setConfirmAction({
|
||||||
|
type: 'commit',
|
||||||
|
message: `Commit ${selectedFiles.size} file${selectedFiles.size !== 1 ? 's' : ''} with message: "${commitMessage.trim()}"?`
|
||||||
|
})}
|
||||||
disabled={!commitMessage.trim() || selectedFiles.size === 0 || isCommitting}
|
disabled={!commitMessage.trim() || selectedFiles.size === 0 || isCommitting}
|
||||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||||
>
|
>
|
||||||
@@ -986,6 +1063,70 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Confirmation Modal */}
|
||||||
|
{confirmAction && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setConfirmAction(null)} />
|
||||||
|
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className={`p-2 rounded-full mr-3 ${
|
||||||
|
confirmAction.type === 'discard' ? 'bg-red-100 dark:bg-red-900' : 'bg-yellow-100 dark:bg-yellow-900'
|
||||||
|
}`}>
|
||||||
|
<AlertTriangle className={`w-5 h-5 ${
|
||||||
|
confirmAction.type === 'discard' ? 'text-red-600 dark:text-red-400' : 'text-yellow-600 dark:text-yellow-400'
|
||||||
|
}`} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
{confirmAction.type === 'discard' ? 'Discard Changes' :
|
||||||
|
confirmAction.type === 'commit' ? 'Confirm Commit' : 'Confirm Pull'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
{confirmAction.message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmAction(null)}
|
||||||
|
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={confirmAndExecute}
|
||||||
|
className={`px-4 py-2 text-sm text-white rounded-md ${
|
||||||
|
confirmAction.type === 'discard'
|
||||||
|
? 'bg-red-600 hover:bg-red-700'
|
||||||
|
: confirmAction.type === 'commit'
|
||||||
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
: 'bg-green-600 hover:bg-green-700'
|
||||||
|
} flex items-center space-x-2`}
|
||||||
|
>
|
||||||
|
{confirmAction.type === 'discard' ? (
|
||||||
|
<>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
<span>Discard</span>
|
||||||
|
</>
|
||||||
|
) : confirmAction.type === 'commit' ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
<span>Commit</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span>Pull</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,27 @@ import { ClipboardAddon } from '@xterm/addon-clipboard';
|
|||||||
import { WebglAddon } from '@xterm/addon-webgl';
|
import { WebglAddon } from '@xterm/addon-webgl';
|
||||||
import 'xterm/css/xterm.css';
|
import 'xterm/css/xterm.css';
|
||||||
|
|
||||||
|
// CSS to remove xterm focus outline
|
||||||
|
const xtermStyles = `
|
||||||
|
.xterm .xterm-screen {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
.xterm:focus .xterm-screen {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
.xterm-screen:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Inject styles
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const styleSheet = document.createElement('style');
|
||||||
|
styleSheet.type = 'text/css';
|
||||||
|
styleSheet.innerText = xtermStyles;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
}
|
||||||
|
|
||||||
// Global store for shell sessions to persist across tab switches
|
// Global store for shell sessions to persist across tab switches
|
||||||
const shellSessions = new Map();
|
const shellSessions = new Map();
|
||||||
|
|
||||||
@@ -138,6 +159,14 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (fitAddon.current) {
|
if (fitAddon.current) {
|
||||||
fitAddon.current.fit();
|
fitAddon.current.fit();
|
||||||
|
// Send terminal size to backend after reattaching
|
||||||
|
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||||
|
ws.current.send(JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: terminal.current.cols,
|
||||||
|
rows: terminal.current.rows
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
@@ -226,6 +255,13 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
|
|
||||||
terminal.current.open(terminalRef.current);
|
terminal.current.open(terminalRef.current);
|
||||||
|
|
||||||
|
// Wait for terminal to be fully rendered, then fit
|
||||||
|
setTimeout(() => {
|
||||||
|
if (fitAddon.current) {
|
||||||
|
fitAddon.current.fit();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
// Add keyboard shortcuts for copy/paste
|
// Add keyboard shortcuts for copy/paste
|
||||||
terminal.current.attachCustomKeyEventHandler((event) => {
|
terminal.current.attachCustomKeyEventHandler((event) => {
|
||||||
// Ctrl+C or Cmd+C for copy (when text is selected)
|
// Ctrl+C or Cmd+C for copy (when text is selected)
|
||||||
@@ -252,10 +288,18 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure terminal takes full space
|
// Ensure terminal takes full space and notify backend of size
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (fitAddon.current) {
|
if (fitAddon.current) {
|
||||||
fitAddon.current.fit();
|
fitAddon.current.fit();
|
||||||
|
// Send terminal size to backend after fitting
|
||||||
|
if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||||
|
ws.current.send(JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: terminal.current.cols,
|
||||||
|
rows: terminal.current.rows
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
@@ -276,6 +320,14 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
if (fitAddon.current && terminal.current) {
|
if (fitAddon.current && terminal.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitAddon.current.fit();
|
fitAddon.current.fit();
|
||||||
|
// Send updated terminal size to backend after resize
|
||||||
|
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||||
|
ws.current.send(JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: terminal.current.cols,
|
||||||
|
rows: terminal.current.rows
|
||||||
|
}));
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -309,10 +361,18 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive || !isInitialized) return;
|
if (!isActive || !isInitialized) return;
|
||||||
|
|
||||||
// Fit terminal when tab becomes active
|
// Fit terminal when tab becomes active and notify backend
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (fitAddon.current) {
|
if (fitAddon.current) {
|
||||||
fitAddon.current.fit();
|
fitAddon.current.fit();
|
||||||
|
// Send terminal size to backend after tab activation
|
||||||
|
if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||||
|
ws.current.send(JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: terminal.current.cols,
|
||||||
|
rows: terminal.current.rows
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}, [isActive, isInitialized]);
|
}, [isActive, isInitialized]);
|
||||||
@@ -363,16 +423,38 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
|
||||||
// Send initial setup with project path and session info
|
// Wait for terminal to be ready, then fit and send dimensions
|
||||||
|
setTimeout(() => {
|
||||||
|
if (fitAddon.current && terminal.current) {
|
||||||
|
// Force a fit to ensure proper dimensions
|
||||||
|
fitAddon.current.fit();
|
||||||
|
|
||||||
|
// Wait a bit more for fit to complete, then send dimensions
|
||||||
|
setTimeout(() => {
|
||||||
const initPayload = {
|
const initPayload = {
|
||||||
type: 'init',
|
type: 'init',
|
||||||
projectPath: selectedProject.fullPath || selectedProject.path,
|
projectPath: selectedProject.fullPath || selectedProject.path,
|
||||||
sessionId: selectedSession?.id,
|
sessionId: selectedSession?.id,
|
||||||
hasSession: !!selectedSession
|
hasSession: !!selectedSession,
|
||||||
|
cols: terminal.current.cols,
|
||||||
|
rows: terminal.current.rows
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
ws.current.send(JSON.stringify(initPayload));
|
ws.current.send(JSON.stringify(initPayload));
|
||||||
|
|
||||||
|
// Also send resize message immediately after init
|
||||||
|
setTimeout(() => {
|
||||||
|
if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||||
|
ws.current.send(JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: terminal.current.cols,
|
||||||
|
rows: terminal.current.rows
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.current.onmessage = (event) => {
|
ws.current.onmessage = (event) => {
|
||||||
@@ -442,7 +524,7 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-gray-900">
|
<div className="h-full flex flex-col bg-gray-900 w-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 bg-gray-800 border-b border-gray-700 px-4 py-2">
|
<div className="flex-shrink-0 bg-gray-800 border-b border-gray-700 px-4 py-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -494,7 +576,7 @@ function Shell({ selectedProject, selectedSession, isActive }) {
|
|||||||
|
|
||||||
{/* Terminal */}
|
{/* Terminal */}
|
||||||
<div className="flex-1 p-2 overflow-hidden relative">
|
<div className="flex-1 p-2 overflow-hidden relative">
|
||||||
<div ref={terminalRef} className="h-full w-full" />
|
<div ref={terminalRef} className="h-full w-full focus:outline-none" style={{ outline: 'none' }} />
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{!isInitialized && (
|
{!isInitialized && (
|
||||||
|
|||||||
@@ -99,8 +99,12 @@
|
|||||||
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Color transitions for theme switching */
|
/* Color transitions for theme switching - exclude interactive elements */
|
||||||
* {
|
body, div, section, article, aside, header, footer, nav, main,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, span, blockquote,
|
||||||
|
ul, ol, li, dl, dt, dd,
|
||||||
|
table, thead, tbody, tfoot, tr, td, th,
|
||||||
|
form, fieldset, legend, label {
|
||||||
transition: background-color 200ms ease-in-out,
|
transition: background-color 200ms ease-in-out,
|
||||||
border-color 200ms ease-in-out,
|
border-color 200ms ease-in-out,
|
||||||
color 200ms ease-in-out;
|
color 200ms ease-in-out;
|
||||||
|
|||||||
Reference in New Issue
Block a user