mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-12 13:19:43 +00:00
Merge branch 'main' into fix/registration-race-condition
This commit is contained in:
@@ -444,10 +444,25 @@ router.get('/remote-status', async (req, res) => {
|
|||||||
trackingBranch = stdout.trim();
|
trackingBranch = stdout.trim();
|
||||||
remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
|
remoteName = trackingBranch.split('/')[0]; // Extract remote name (e.g., "origin/main" -> "origin")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// No upstream branch configured
|
// No upstream branch configured - but check if we have remotes
|
||||||
|
let hasRemote = false;
|
||||||
|
let remoteName = null;
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('git remote', { cwd: projectPath });
|
||||||
|
const remotes = stdout.trim().split('\n').filter(r => r.trim());
|
||||||
|
if (remotes.length > 0) {
|
||||||
|
hasRemote = true;
|
||||||
|
remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
|
||||||
|
}
|
||||||
|
} catch (remoteError) {
|
||||||
|
// No remotes configured
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
hasRemote: false,
|
hasRemote,
|
||||||
|
hasUpstream: false,
|
||||||
branch,
|
branch,
|
||||||
|
remoteName,
|
||||||
message: 'No remote tracking branch configured'
|
message: 'No remote tracking branch configured'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -462,6 +477,7 @@ router.get('/remote-status', async (req, res) => {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
hasRemote: true,
|
hasRemote: true,
|
||||||
|
hasUpstream: true,
|
||||||
branch,
|
branch,
|
||||||
remoteBranch: trackingBranch,
|
remoteBranch: trackingBranch,
|
||||||
remoteName,
|
remoteName,
|
||||||
@@ -653,6 +669,82 @@ router.post('/push', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Publish branch to remote (set upstream and push)
|
||||||
|
router.post('/publish', async (req, res) => {
|
||||||
|
const { project, branch } = req.body;
|
||||||
|
|
||||||
|
if (!project || !branch) {
|
||||||
|
return res.status(400).json({ error: 'Project name and branch are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectPath = await getActualProjectPath(project);
|
||||||
|
await validateGitRepository(projectPath);
|
||||||
|
|
||||||
|
// Get current branch to verify it matches the requested branch
|
||||||
|
const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: projectPath });
|
||||||
|
const currentBranchName = currentBranch.trim();
|
||||||
|
|
||||||
|
if (currentBranchName !== branch) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Branch mismatch. Current branch is ${currentBranchName}, but trying to publish ${branch}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if remote exists
|
||||||
|
let remoteName = 'origin';
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('git remote', { cwd: projectPath });
|
||||||
|
const remotes = stdout.trim().split('\n').filter(r => r.trim());
|
||||||
|
if (remotes.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
remoteName = remotes.includes('origin') ? 'origin' : remotes[0];
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'No remote repository configured. Add a remote with: git remote add origin <url>'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish the branch (set upstream and push)
|
||||||
|
const { stdout } = await execAsync(`git push --set-upstream ${remoteName} ${branch}`, { cwd: projectPath });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
output: stdout || 'Branch published successfully',
|
||||||
|
remoteName,
|
||||||
|
branch
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Git publish error:', error);
|
||||||
|
|
||||||
|
// Enhanced error handling for common publish scenarios
|
||||||
|
let errorMessage = 'Publish failed';
|
||||||
|
let details = error.message;
|
||||||
|
|
||||||
|
if (error.message.includes('rejected')) {
|
||||||
|
errorMessage = 'Publish rejected';
|
||||||
|
details = 'The remote branch already exists and has different commits. Use push instead.';
|
||||||
|
} else if (error.message.includes('Could not resolve hostname')) {
|
||||||
|
errorMessage = 'Network error';
|
||||||
|
details = 'Unable to connect to remote repository. Check your internet connection.';
|
||||||
|
} else if (error.message.includes('Permission denied')) {
|
||||||
|
errorMessage = 'Authentication failed';
|
||||||
|
details = 'Permission denied. Check your credentials or SSH keys.';
|
||||||
|
} else if (error.message.includes('fatal:') && error.message.includes('does not appear to be a git repository')) {
|
||||||
|
errorMessage = 'Remote not configured';
|
||||||
|
details = 'Remote repository not properly configured. Check your remote URL.';
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
error: errorMessage,
|
||||||
|
details: details
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Discard changes for a specific file
|
// Discard changes for a specific file
|
||||||
router.post('/discard', async (req, res) => {
|
router.post('/discard', async (req, res) => {
|
||||||
const { project, file } = req.body;
|
const { project, file } = req.body;
|
||||||
@@ -692,4 +784,39 @@ router.post('/discard', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete untracked file
|
||||||
|
router.post('/delete-untracked', 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 if file is actually untracked
|
||||||
|
const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath });
|
||||||
|
|
||||||
|
if (!statusOutput.trim()) {
|
||||||
|
return res.status(400).json({ error: 'File is not untracked or does not exist' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = statusOutput.substring(0, 2);
|
||||||
|
|
||||||
|
if (status !== '??') {
|
||||||
|
return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the untracked file
|
||||||
|
await fs.unlink(path.join(projectPath, file));
|
||||||
|
|
||||||
|
res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Git delete untracked error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
10
src/App.jsx
10
src/App.jsx
@@ -64,6 +64,10 @@ function AppContent() {
|
|||||||
const saved = localStorage.getItem('autoScrollToBottom');
|
const saved = localStorage.getItem('autoScrollToBottom');
|
||||||
return saved !== null ? JSON.parse(saved) : true;
|
return saved !== null ? JSON.parse(saved) : true;
|
||||||
});
|
});
|
||||||
|
const [sendByCtrlEnter, setSendByCtrlEnter] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('sendByCtrlEnter');
|
||||||
|
return saved !== null ? JSON.parse(saved) : false;
|
||||||
|
});
|
||||||
// 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
|
||||||
@@ -586,6 +590,7 @@ function AppContent() {
|
|||||||
autoExpandTools={autoExpandTools}
|
autoExpandTools={autoExpandTools}
|
||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -617,6 +622,11 @@ function AppContent() {
|
|||||||
setAutoScrollToBottom(value);
|
setAutoScrollToBottom(value);
|
||||||
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
|
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
|
||||||
}}
|
}}
|
||||||
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
|
onSendByCtrlEnterChange={(value) => {
|
||||||
|
setSendByCtrlEnter(value);
|
||||||
|
localStorage.setItem('sendByCtrlEnter', JSON.stringify(value));
|
||||||
|
}}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
|
||||||
{message.isToolUse ? (
|
{message.isToolUse && !['Read', 'TodoWrite', 'TodoRead'].includes(message.toolName) ? (
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-2 sm:p-3 mb-2">
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-2 sm:p-3 mb-2">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -423,41 +423,18 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
try {
|
try {
|
||||||
const input = JSON.parse(message.toolInput);
|
const input = JSON.parse(message.toolInput);
|
||||||
if (input.file_path) {
|
if (input.file_path) {
|
||||||
// Extract filename
|
|
||||||
const filename = input.file_path.split('/').pop();
|
const filename = input.file_path.split('/').pop();
|
||||||
const pathParts = input.file_path.split('/');
|
|
||||||
const directoryPath = pathParts.slice(0, -1).join('/');
|
|
||||||
|
|
||||||
// Simple heuristic to show only relevant path parts
|
|
||||||
// Show the last 2-3 directory parts before the filename
|
|
||||||
const relevantParts = pathParts.slice(-4, -1); // Get up to 3 directories before filename
|
|
||||||
const relativePath = relevantParts.length > 0 ? relevantParts.join('/') + '/' : '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<details className="mt-2" open={autoExpandTools}>
|
<div className="mt-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
<summary className="text-sm text-blue-700 dark:text-blue-300 cursor-pointer hover:text-blue-800 dark:hover:text-blue-200 flex items-center gap-1">
|
Read{' '}
|
||||||
<svg className="w-4 h-4 transition-transform details-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<button
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
onClick={() => onFileOpen && onFileOpen(input.file_path)}
|
||||||
</svg>
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono"
|
||||||
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
{filename}
|
||||||
</svg>
|
</button>
|
||||||
<span className="text-gray-600 dark:text-gray-400 font-mono text-xs">{relativePath}</span>
|
</div>
|
||||||
<span className="font-semibold text-blue-700 dark:text-blue-300 font-mono">{filename}</span>
|
|
||||||
</summary>
|
|
||||||
{showRawParameters && (
|
|
||||||
<div className="mt-3">
|
|
||||||
<details className="mt-2">
|
|
||||||
<summary className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer hover:text-blue-700 dark:hover:text-blue-300">
|
|
||||||
View raw parameters
|
|
||||||
</summary>
|
|
||||||
<pre className="mt-2 text-xs bg-blue-100 dark:bg-blue-800/30 p-2 rounded whitespace-pre-wrap break-words overflow-hidden text-blue-900 dark:text-blue-100">
|
|
||||||
{message.toolInput}
|
|
||||||
</pre>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</details>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -882,6 +859,61 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : message.isToolUse && message.toolName === 'Read' ? (
|
||||||
|
// Simple Read tool indicator
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const input = JSON.parse(message.toolInput);
|
||||||
|
if (input.file_path) {
|
||||||
|
const filename = input.file_path.split('/').pop();
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
📖 Read{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => onFileOpen && onFileOpen(input.file_path)}
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline font-mono"
|
||||||
|
>
|
||||||
|
{filename}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
📖 Read file
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
) : message.isToolUse && message.toolName === 'TodoWrite' ? (
|
||||||
|
// Simple TodoWrite tool indicator with tasks
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const input = JSON.parse(message.toolInput);
|
||||||
|
if (input.todos && Array.isArray(input.todos)) {
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2">
|
||||||
|
<div className="text-sm text-blue-700 dark:text-blue-300 mb-2">
|
||||||
|
📝 Update todo list
|
||||||
|
</div>
|
||||||
|
<TodoList todos={input.todos} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
📝 Update todo list
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
) : message.isToolUse && message.toolName === 'TodoRead' ? (
|
||||||
|
// Simple TodoRead tool indicator
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 border-l-2 border-blue-300 dark:border-blue-600 pl-3 py-1 mb-2 text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
📋 Read todo list
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
{message.type === 'assistant' ? (
|
{message.type === 'assistant' ? (
|
||||||
@@ -984,7 +1016,7 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
|
|||||||
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
|
// - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID
|
||||||
//
|
//
|
||||||
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
|
// This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations.
|
||||||
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom }) {
|
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) {
|
||||||
const [input, setInput] = useState(() => {
|
const [input, setInput] = useState(() => {
|
||||||
if (typeof window !== 'undefined' && selectedProject) {
|
if (typeof window !== 'undefined' && selectedProject) {
|
||||||
return localStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
return localStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
||||||
@@ -1400,19 +1432,45 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
toolResult: null // Will be updated when result comes in
|
toolResult: null // Will be updated when result comes in
|
||||||
}]);
|
}]);
|
||||||
} else if (part.type === 'text' && part.text?.trim()) {
|
} else if (part.type === 'text' && part.text?.trim()) {
|
||||||
|
// Check for usage limit message and format it user-friendly
|
||||||
|
let content = part.text;
|
||||||
|
if (content.includes('Claude AI usage limit reached|')) {
|
||||||
|
const parts = content.split('|');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const timestamp = parseInt(parts[1]);
|
||||||
|
if (!isNaN(timestamp)) {
|
||||||
|
const resetTime = new Date(timestamp * 1000);
|
||||||
|
content = `Claude AI usage limit reached. The limit will reset on ${resetTime.toLocaleDateString()} at ${resetTime.toLocaleTimeString()}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add regular text message
|
// Add regular text message
|
||||||
setChatMessages(prev => [...prev, {
|
setChatMessages(prev => [...prev, {
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
content: part.text,
|
content: content,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof messageData.content === 'string' && messageData.content.trim()) {
|
} else if (typeof messageData.content === 'string' && messageData.content.trim()) {
|
||||||
|
// Check for usage limit message and format it user-friendly
|
||||||
|
let content = messageData.content;
|
||||||
|
if (content.includes('Claude AI usage limit reached|')) {
|
||||||
|
const parts = content.split('|');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
const timestamp = parseInt(parts[1]);
|
||||||
|
if (!isNaN(timestamp)) {
|
||||||
|
const resetTime = new Date(timestamp * 1000);
|
||||||
|
content = `Claude AI usage limit reached. The limit will reset on ${resetTime.toLocaleDateString()} at ${resetTime.toLocaleTimeString()}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add regular text message
|
// Add regular text message
|
||||||
setChatMessages(prev => [...prev, {
|
setChatMessages(prev => [...prev, {
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
content: messageData.content,
|
content: content,
|
||||||
timestamp: new Date()
|
timestamp: new Date()
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
@@ -1966,14 +2024,21 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
// Handle Enter key: Ctrl+Enter (Cmd+Enter on Mac) sends, Shift+Enter creates new line
|
// Handle Enter key: Ctrl+Enter (Cmd+Enter on Mac) sends, Shift+Enter creates new line
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
// If we're in composition, don't send message
|
||||||
|
if (e.nativeEvent.isComposing) {
|
||||||
|
return; // Let IME handle the Enter key
|
||||||
|
}
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
// Ctrl+Enter or Cmd+Enter: Send message
|
// Ctrl+Enter or Cmd+Enter: Send message
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit(e);
|
handleSubmit(e);
|
||||||
} else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
} else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||||
// Plain Enter: Also send message (keeping original behavior)
|
// Plain Enter: Send message only if not in IME composition
|
||||||
e.preventDefault();
|
if (!sendByCtrlEnter) {
|
||||||
handleSubmit(e);
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Shift+Enter: Allow default behavior (new line)
|
// Shift+Enter: Allow default behavior (new line)
|
||||||
}
|
}
|
||||||
@@ -2404,12 +2469,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
</div>
|
</div>
|
||||||
{/* Hint text */}
|
{/* Hint text */}
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2 hidden sm:block">
|
<div className="text-xs text-gray-500 dark:text-gray-400 text-center mt-2 hidden sm:block">
|
||||||
Press Enter to send • Shift+Enter for new line • Tab to change modes • @ to reference files
|
{sendByCtrlEnter
|
||||||
|
? "Ctrl+Enter to send (IME safe) • Shift+Enter for new line • Tab to change modes • @ to reference files"
|
||||||
|
: "Press Enter to send • Shift+Enter for new line • Tab to change modes • @ to reference files"}
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-xs text-gray-500 dark:text-gray-400 text-center mt-2 sm:hidden transition-opacity duration-200 ${
|
<div className={`text-xs text-gray-500 dark:text-gray-400 text-center mt-2 sm:hidden transition-opacity duration-200 ${
|
||||||
isInputFocused ? 'opacity-100' : 'opacity-0'
|
isInputFocused ? 'opacity-100' : 'opacity-0'
|
||||||
}`}>
|
}`}>
|
||||||
Enter to send • Tab for modes • @ for files
|
{sendByCtrlEnter
|
||||||
|
? "Ctrl+Enter to send (IME safe) • Tab for modes • @ for files"
|
||||||
|
: "Enter to send • Tab for modes • @ for files"}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 [isPushing, setIsPushing] = useState(false);
|
const [isPushing, setIsPushing] = useState(false);
|
||||||
|
const [isPublishing, setIsPublishing] = 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|push', file?: string, message?: string }
|
const [confirmAction, setConfirmAction] = useState(null); // { type: 'discard|commit|pull|push', file?: string, message?: string }
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
@@ -266,6 +267,34 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePublish = async () => {
|
||||||
|
setIsPublishing(true);
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch('/api/git/publish', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
project: selectedProject.name,
|
||||||
|
branch: currentBranch
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
// Refresh status after successful publish
|
||||||
|
fetchGitStatus();
|
||||||
|
fetchRemoteStatus();
|
||||||
|
} else {
|
||||||
|
console.error('Publish failed:', data.error);
|
||||||
|
// TODO: Show user-friendly error message
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error publishing branch:', error);
|
||||||
|
} finally {
|
||||||
|
setIsPublishing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const discardChanges = async (filePath) => {
|
const discardChanges = async (filePath) => {
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch('/api/git/discard', {
|
const response = await authenticatedFetch('/api/git/discard', {
|
||||||
@@ -294,6 +323,34 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteUntrackedFile = async (filePath) => {
|
||||||
|
try {
|
||||||
|
const response = await authenticatedFetch('/api/git/delete-untracked', {
|
||||||
|
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('Delete failed:', data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting untracked file:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmAndExecute = async () => {
|
const confirmAndExecute = async () => {
|
||||||
if (!confirmAction) return;
|
if (!confirmAction) return;
|
||||||
|
|
||||||
@@ -305,6 +362,9 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
case 'discard':
|
case 'discard':
|
||||||
await discardChanges(file);
|
await discardChanges(file);
|
||||||
break;
|
break;
|
||||||
|
case 'delete':
|
||||||
|
await deleteUntrackedFile(file);
|
||||||
|
break;
|
||||||
case 'commit':
|
case 'commit':
|
||||||
await handleCommit();
|
await handleCommit();
|
||||||
break;
|
break;
|
||||||
@@ -314,6 +374,9 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
case 'push':
|
case 'push':
|
||||||
await handlePush();
|
await handlePush();
|
||||||
break;
|
break;
|
||||||
|
case 'publish':
|
||||||
|
await handlePublish();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error executing ${type}:`, error);
|
console.error(`Error executing ${type}:`, error);
|
||||||
@@ -578,6 +641,23 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
{isMobile && <span>Discard</span>}
|
{isMobile && <span>Discard</span>}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{status === 'U' && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setConfirmAction({
|
||||||
|
type: 'delete',
|
||||||
|
file: filePath,
|
||||||
|
message: `Delete untracked file "${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="Delete untracked file"
|
||||||
|
>
|
||||||
|
<Trash2 className={`${isMobile ? 'w-3 h-3' : 'w-3 h-3'}`} />
|
||||||
|
{isMobile && <span>Delete</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' :
|
||||||
@@ -716,51 +796,72 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
|
|
||||||
<div className={`flex items-center ${isMobile ? 'gap-1' : 'gap-2'}`}>
|
<div className={`flex items-center ${isMobile ? 'gap-1' : 'gap-2'}`}>
|
||||||
{/* Remote action buttons - smart logic based on ahead/behind status */}
|
{/* Remote action buttons - smart logic based on ahead/behind status */}
|
||||||
{remoteStatus?.hasRemote && !remoteStatus?.isUpToDate && (
|
{remoteStatus?.hasRemote && (
|
||||||
<>
|
<>
|
||||||
{/* Pull button - show when behind (primary action) */}
|
{/* Publish button - show when branch doesn't exist on remote */}
|
||||||
{remoteStatus.behind > 0 && (
|
{!remoteStatus?.hasUpstream && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmAction({
|
onClick={() => setConfirmAction({
|
||||||
type: 'pull',
|
type: 'publish',
|
||||||
message: `Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}?`
|
message: `Publish branch "${currentBranch}" to ${remoteStatus.remoteName}?`
|
||||||
})}
|
})}
|
||||||
disabled={isPulling}
|
disabled={isPublishing}
|
||||||
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-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50 flex items-center gap-1"
|
||||||
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
title={`Publish branch "${currentBranch}" to ${remoteStatus.remoteName}`}
|
||||||
>
|
>
|
||||||
<Download className={`w-3 h-3 ${isPulling ? 'animate-pulse' : ''}`} />
|
<Upload className={`w-3 h-3 ${isPublishing ? 'animate-pulse' : ''}`} />
|
||||||
<span>{isPulling ? 'Pulling...' : `Pull ${remoteStatus.behind}`}</span>
|
<span>{isPublishing ? 'Publishing...' : 'Publish'}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Push button - show when ahead (primary action when ahead only) */}
|
{/* Show normal push/pull buttons only if branch has upstream */}
|
||||||
{remoteStatus.ahead > 0 && (
|
{remoteStatus?.hasUpstream && !remoteStatus?.isUpToDate && (
|
||||||
<button
|
<>
|
||||||
onClick={() => setConfirmAction({
|
{/* Pull button - show when behind (primary action) */}
|
||||||
type: 'push',
|
{remoteStatus.behind > 0 && (
|
||||||
message: `Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}?`
|
<button
|
||||||
})}
|
onClick={() => setConfirmAction({
|
||||||
disabled={isPushing}
|
type: 'pull',
|
||||||
className="px-2 py-1 text-xs bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50 flex items-center gap-1"
|
message: `Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}?`
|
||||||
title={`Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}`}
|
})}
|
||||||
>
|
disabled={isPulling}
|
||||||
<Upload className={`w-3 h-3 ${isPushing ? 'animate-pulse' : ''}`} />
|
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"
|
||||||
<span>{isPushing ? 'Pushing...' : `Push ${remoteStatus.ahead}`}</span>
|
title={`Pull ${remoteStatus.behind} commit${remoteStatus.behind !== 1 ? 's' : ''} from ${remoteStatus.remoteName}`}
|
||||||
</button>
|
>
|
||||||
)}
|
<Download className={`w-3 h-3 ${isPulling ? 'animate-pulse' : ''}`} />
|
||||||
|
<span>{isPulling ? 'Pulling...' : `Pull ${remoteStatus.behind}`}</span>
|
||||||
{/* Fetch button - show when ahead only or when diverged (secondary action) */}
|
</button>
|
||||||
{(remoteStatus.ahead > 0 || (remoteStatus.behind > 0 && remoteStatus.ahead > 0)) && (
|
)}
|
||||||
<button
|
|
||||||
onClick={handleFetch}
|
{/* Push button - show when ahead (primary action when ahead only) */}
|
||||||
disabled={isFetching}
|
{remoteStatus.ahead > 0 && (
|
||||||
className="px-2 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
|
<button
|
||||||
title={`Fetch from ${remoteStatus.remoteName}`}
|
onClick={() => setConfirmAction({
|
||||||
>
|
type: 'push',
|
||||||
<RefreshCw className={`w-3 h-3 ${isFetching ? 'animate-spin' : ''}`} />
|
message: `Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}?`
|
||||||
<span>{isFetching ? 'Fetching...' : 'Fetch'}</span>
|
})}
|
||||||
</button>
|
disabled={isPushing}
|
||||||
|
className="px-2 py-1 text-xs bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50 flex items-center gap-1"
|
||||||
|
title={`Push ${remoteStatus.ahead} commit${remoteStatus.ahead !== 1 ? 's' : ''} to ${remoteStatus.remoteName}`}
|
||||||
|
>
|
||||||
|
<Upload className={`w-3 h-3 ${isPushing ? 'animate-pulse' : ''}`} />
|
||||||
|
<span>{isPushing ? 'Pushing...' : `Push ${remoteStatus.ahead}`}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fetch button - show when ahead only or when diverged (secondary action) */}
|
||||||
|
{(remoteStatus.ahead > 0 || (remoteStatus.behind > 0 && remoteStatus.ahead > 0)) && (
|
||||||
|
<button
|
||||||
|
onClick={handleFetch}
|
||||||
|
disabled={isFetching}
|
||||||
|
className="px-2 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
|
||||||
|
title={`Fetch from ${remoteStatus.remoteName}`}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3 h-3 ${isFetching ? 'animate-spin' : ''}`} />
|
||||||
|
<span>{isFetching ? 'Fetching...' : 'Fetch'}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1120,16 +1221,18 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<div className={`p-2 rounded-full mr-3 ${
|
<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'
|
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'bg-red-100 dark:bg-red-900' : 'bg-yellow-100 dark:bg-yellow-900'
|
||||||
}`}>
|
}`}>
|
||||||
<AlertTriangle className={`w-5 h-5 ${
|
<AlertTriangle className={`w-5 h-5 ${
|
||||||
confirmAction.type === 'discard' ? 'text-red-600 dark:text-red-400' : 'text-yellow-600 dark:text-yellow-400'
|
(confirmAction.type === 'discard' || confirmAction.type === 'delete') ? 'text-red-600 dark:text-red-400' : 'text-yellow-600 dark:text-yellow-400'
|
||||||
}`} />
|
}`} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold">
|
||||||
{confirmAction.type === 'discard' ? 'Discard Changes' :
|
{confirmAction.type === 'discard' ? 'Discard Changes' :
|
||||||
|
confirmAction.type === 'delete' ? 'Delete File' :
|
||||||
confirmAction.type === 'commit' ? 'Confirm Commit' :
|
confirmAction.type === 'commit' ? 'Confirm Commit' :
|
||||||
confirmAction.type === 'pull' ? 'Confirm Pull' : 'Confirm Push'}
|
confirmAction.type === 'pull' ? 'Confirm Pull' :
|
||||||
|
confirmAction.type === 'publish' ? 'Publish Branch' : 'Confirm Push'}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1147,12 +1250,14 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<button
|
<button
|
||||||
onClick={confirmAndExecute}
|
onClick={confirmAndExecute}
|
||||||
className={`px-4 py-2 text-sm text-white rounded-md ${
|
className={`px-4 py-2 text-sm text-white rounded-md ${
|
||||||
confirmAction.type === 'discard'
|
(confirmAction.type === 'discard' || confirmAction.type === 'delete')
|
||||||
? 'bg-red-600 hover:bg-red-700'
|
? 'bg-red-600 hover:bg-red-700'
|
||||||
: confirmAction.type === 'commit'
|
: confirmAction.type === 'commit'
|
||||||
? 'bg-blue-600 hover:bg-blue-700'
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
: confirmAction.type === 'pull'
|
: confirmAction.type === 'pull'
|
||||||
? 'bg-green-600 hover:bg-green-700'
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
|
: confirmAction.type === 'publish'
|
||||||
|
? 'bg-purple-600 hover:bg-purple-700'
|
||||||
: 'bg-orange-600 hover:bg-orange-700'
|
: 'bg-orange-600 hover:bg-orange-700'
|
||||||
} flex items-center space-x-2`}
|
} flex items-center space-x-2`}
|
||||||
>
|
>
|
||||||
@@ -1161,6 +1266,11 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
<span>Discard</span>
|
<span>Discard</span>
|
||||||
</>
|
</>
|
||||||
|
) : confirmAction.type === 'delete' ? (
|
||||||
|
<>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
<span>Delete</span>
|
||||||
|
</>
|
||||||
) : confirmAction.type === 'commit' ? (
|
) : confirmAction.type === 'commit' ? (
|
||||||
<>
|
<>
|
||||||
<Check className="w-4 h-4" />
|
<Check className="w-4 h-4" />
|
||||||
@@ -1171,6 +1281,11 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
<span>Pull</span>
|
<span>Pull</span>
|
||||||
</>
|
</>
|
||||||
|
) : confirmAction.type === 'publish' ? (
|
||||||
|
<>
|
||||||
|
<Upload className="w-4 h-4" />
|
||||||
|
<span>Publish</span>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Upload className="w-4 h-4" />
|
<Upload className="w-4 h-4" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import ClaudeLogo from './ClaudeLogo';
|
import { MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@@ -37,7 +37,9 @@ const LoginForm = () => {
|
|||||||
{/* Logo and Title */}
|
{/* Logo and Title */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<ClaudeLogo size={64} />
|
<div className="w-16 h-16 bg-primary rounded-lg flex items-center justify-center shadow-sm">
|
||||||
|
<MessageSquare className="w-8 h-8 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-foreground">Welcome Back</h1>
|
<h1 className="text-2xl font-bold text-foreground">Welcome Back</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function MainContent({
|
|||||||
onShowSettings, // Show tools settings panel
|
onShowSettings, // Show tools settings panel
|
||||||
autoExpandTools, // Auto-expand tool accordions
|
autoExpandTools, // Auto-expand tool accordions
|
||||||
showRawParameters, // Show raw parameters in tool accordions
|
showRawParameters, // Show raw parameters in tool accordions
|
||||||
autoScrollToBottom // Auto-scroll to bottom when new messages arrive
|
autoScrollToBottom, // Auto-scroll to bottom when new messages arrive
|
||||||
|
sendByCtrlEnter // Send by Ctrl+Enter mode for East Asian language input
|
||||||
}) {
|
}) {
|
||||||
const [editingFile, setEditingFile] = useState(null);
|
const [editingFile, setEditingFile] = useState(null);
|
||||||
|
|
||||||
@@ -285,6 +286,7 @@ function MainContent({
|
|||||||
autoExpandTools={autoExpandTools}
|
autoExpandTools={autoExpandTools}
|
||||||
showRawParameters={showRawParameters}
|
showRawParameters={showRawParameters}
|
||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`h-full overflow-hidden ${activeTab === 'files' ? 'block' : 'hidden'}`}>
|
<div className={`h-full overflow-hidden ${activeTab === 'files' ? 'block' : 'hidden'}`}>
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import React from 'react';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import SetupForm from './SetupForm';
|
import SetupForm from './SetupForm';
|
||||||
import LoginForm from './LoginForm';
|
import LoginForm from './LoginForm';
|
||||||
import ClaudeLogo from './ClaudeLogo';
|
import { MessageSquare } from 'lucide-react';
|
||||||
|
|
||||||
const LoadingScreen = () => (
|
const LoadingScreen = () => (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<ClaudeLogo size={64} />
|
<div className="w-16 h-16 bg-primary rounded-lg flex items-center justify-center shadow-sm">
|
||||||
|
<MessageSquare className="w-8 h-8 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-foreground mb-2">Claude Code UI</h1>
|
<h1 className="text-2xl font-bold text-foreground mb-2">Claude Code UI</h1>
|
||||||
<div className="flex items-center justify-center space-x-2">
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
Mic,
|
Mic,
|
||||||
Brain,
|
Brain,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
FileText
|
FileText,
|
||||||
|
Languages
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import DarkModeToggle from './DarkModeToggle';
|
import DarkModeToggle from './DarkModeToggle';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
@@ -25,6 +26,8 @@ const QuickSettingsPanel = ({
|
|||||||
onShowRawParametersChange,
|
onShowRawParametersChange,
|
||||||
autoScrollToBottom,
|
autoScrollToBottom,
|
||||||
onAutoScrollChange,
|
onAutoScrollChange,
|
||||||
|
sendByCtrlEnter,
|
||||||
|
onSendByCtrlEnterChange,
|
||||||
isMobile
|
isMobile
|
||||||
}) => {
|
}) => {
|
||||||
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
|
const [localIsOpen, setLocalIsOpen] = useState(isOpen);
|
||||||
@@ -142,6 +145,27 @@ const QuickSettingsPanel = ({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Input Settings */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Input Settings</h4>
|
||||||
|
|
||||||
|
<label className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors border border-transparent hover:border-gray-300 dark:hover:border-gray-600">
|
||||||
|
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
|
||||||
|
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||||
|
Send by Ctrl+Enter
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={sendByCtrlEnter}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 ml-3">
|
||||||
|
When enabled, pressing Ctrl+Enter will send the message instead of just Enter. This is useful for IME users to avoid accidental sends.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Whisper Dictation Settings - HIDDEN */}
|
{/* Whisper Dictation Settings - HIDDEN */}
|
||||||
<div className="space-y-2" style={{ display: 'none' }}>
|
<div className="space-y-2" style={{ display: 'none' }}>
|
||||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
<h4 className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">Whisper Dictation</h4>
|
||||||
|
|||||||
@@ -374,9 +374,7 @@ function Sidebar({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const currentSessionCount = (project.sessions?.length || 0) + (additionalSessions[project.name]?.length || 0);
|
const currentSessionCount = (project.sessions?.length || 0) + (additionalSessions[project.name]?.length || 0);
|
||||||
const response = await fetch(
|
const response = await api.sessions(project.name, 5, currentSessionCount);
|
||||||
`/api/projects/${project.name}/sessions?limit=5&offset=${currentSessionCount}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user