Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions ccui.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
#!/bin/bash

# Claude Code UI Control Script
# Usage: ccui.sh [start|stop]

PROJECT_DIR="/Users/alexsuprun/Documents/my-code/claudecodeui"
SERVER_PORT=3001
CLIENT_PORT=5173
LOG_FILE="$PROJECT_DIR/ccui.log"
PID_FILE="$PROJECT_DIR/ccui.pid"

start() {
echo "🚀 Starting Claude Code UI..."

# Check if already running
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "⚠️ Claude Code UI is already running (PID: $PID)"
echo "💡 Use 'ccui stop' to stop it first"
return 1
else
# PID file exists but process is dead, clean it up
rm "$PID_FILE"
fi
fi

# Check and kill processes on both ports
for PORT in $SERVER_PORT $CLIENT_PORT; do
PORT_PIDS=$(lsof -ti:$PORT 2>/dev/null)
if [ ! -z "$PORT_PIDS" ]; then
echo "⚠️ Port $PORT is in use by processes: $PORT_PIDS"
echo "🔧 Killing processes on port $PORT..."
kill -9 $PORT_PIDS 2>/dev/null
sleep 1
fi
done

# Ensure .env exists
if [ ! -f "$PROJECT_DIR/.env" ]; then
echo "📝 Creating .env from .env.example..."
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
fi

# Start the dev server (both backend and frontend) in background
cd "$PROJECT_DIR"
echo "📦 Starting development servers (backend + frontend)..."
nohup npm run dev > "$LOG_FILE" 2>&1 &

# Save PID
echo $! > "$PID_FILE"

echo ""
echo "⏳ Waiting for servers to start..."

# Wait for both server and client to be ready
MAX_WAIT=30
WAIT_COUNT=0
SERVER_READY=false
CLIENT_READY=false

while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
# Check server port
if ! $SERVER_READY && lsof -ti:$SERVER_PORT > /dev/null 2>&1; then
echo "✅ Backend server ready on port $SERVER_PORT"
SERVER_READY=true
fi

# Check client port
if ! $CLIENT_READY && lsof -ti:$CLIENT_PORT > /dev/null 2>&1; then
echo "✅ Frontend client ready on port $CLIENT_PORT"
CLIENT_READY=true
fi

# Break if both are ready
if $SERVER_READY && $CLIENT_READY; then
break
fi

sleep 1
WAIT_COUNT=$((WAIT_COUNT + 1))
done

echo ""
if $SERVER_READY && $CLIENT_READY; then
echo "✅ Claude Code UI started successfully!"
echo "🌐 Server: http://localhost:$SERVER_PORT"
echo "🎨 Client: http://localhost:$CLIENT_PORT"
echo "📝 Logs: $LOG_FILE"
echo "💡 Use 'ccui stop' to stop all servers"
echo ""

# Open browser
echo "🌐 Opening browser..."
if command -v open > /dev/null 2>&1; then
open "http://localhost:$CLIENT_PORT"
elif command -v xdg-open > /dev/null 2>&1; then
xdg-open "http://localhost:$CLIENT_PORT"
else
echo "⚠️ Could not auto-open browser. Please open http://localhost:$CLIENT_PORT manually"
fi

echo ""
echo "📊 Recent logs:"
tail -n 10 "$LOG_FILE"
else
echo "⚠️ Servers may not have started properly within $MAX_WAIT seconds"
echo "📝 Check logs for details: $LOG_FILE"
echo "💡 Or run: ccui logs"
fi
}

stop() {
echo "🛑 Stopping Claude Code UI..."

if [ ! -f "$PID_FILE" ]; then
echo "⚠️ No PID file found"

# Try to kill by ports
for PORT in $SERVER_PORT $CLIENT_PORT; do
PORT_PIDS=$(lsof -ti:$PORT 2>/dev/null)
if [ ! -z "$PORT_PIDS" ]; then
echo "🔧 Found processes on port $PORT: $PORT_PIDS"
kill -9 $PORT_PIDS 2>/dev/null
echo "✅ Killed processes on port $PORT"
fi
done
return 0
fi

PID=$(cat "$PID_FILE")

# Kill the main process and its children
if ps -p "$PID" > /dev/null 2>&1; then
echo "🔧 Killing process tree for PID $PID..."

# Kill child processes first
pkill -P "$PID" 2>/dev/null

# Kill main process
kill "$PID" 2>/dev/null
sleep 1

# Force kill if still running
if ps -p "$PID" > /dev/null 2>&1; then
echo "⚡ Force killing process $PID..."
kill -9 "$PID" 2>/dev/null
fi

echo "✅ Process stopped"
else
echo "ℹ️ Process $PID is not running"
fi

# Clean up any remaining processes on both ports
for PORT in $SERVER_PORT $CLIENT_PORT; do
PORT_PIDS=$(lsof -ti:$PORT 2>/dev/null)
if [ ! -z "$PORT_PIDS" ]; then
echo "🔧 Cleaning up remaining processes on port $PORT..."
kill -9 $PORT_PIDS 2>/dev/null
fi
done

# Remove PID file
rm -f "$PID_FILE"
echo "🧹 Cleaned up PID file"
}

status() {
echo "📊 Claude Code UI Status"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "✅ Running (PID: $PID)"
echo "🌐 Server: http://localhost:$SERVER_PORT"
echo "🎨 Client: http://localhost:$CLIENT_PORT"
echo "📝 Logs: $LOG_FILE"
else
echo "❌ Not running (stale PID file)"
fi
else
echo "❌ Not running"
fi

echo ""
echo "Port Status:"

# Check server port
SERVER_PIDS=$(lsof -ti:$SERVER_PORT 2>/dev/null)
if [ ! -z "$SERVER_PIDS" ]; then
echo "🔌 Server port $SERVER_PORT in use by: $SERVER_PIDS"
else
echo "🔌 Server port $SERVER_PORT is free"
fi

# Check client port
CLIENT_PIDS=$(lsof -ti:$CLIENT_PORT 2>/dev/null)
if [ ! -z "$CLIENT_PIDS" ]; then
echo "🔌 Client port $CLIENT_PORT in use by: $CLIENT_PIDS"
else
echo "🔌 Client port $CLIENT_PORT is free"
fi
}

# Main command handler
COMMAND=${1:-start}

case "$COMMAND" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 2
start
;;
status)
status
;;
logs)
if [ -f "$LOG_FILE" ]; then
tail -f "$LOG_FILE"
else
echo "❌ No log file found at $LOG_FILE"
fi
;;
*)
echo "Usage: ccui.sh [start|stop|restart|status|logs]"
echo ""
echo "Commands:"
echo " start - Start the server (default)"
echo " stop - Stop the server"
echo " restart - Restart the server"
echo " status - Check server status"
echo " logs - Follow server logs"
exit 1
;;
esac
22 changes: 17 additions & 5 deletions src/components/ChatInterface.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MicButton } from './MicButton.jsx';
import { api, authenticatedFetch } from '../utils/api';
import Fuse from 'fuse.js';
import CommandMenu from './CommandMenu';
import { hasRTLCharacters } from '../utils/rtlDetection';


// Helper function to decode HTML entities in text
Expand Down Expand Up @@ -91,13 +92,13 @@ function unescapeWithMathProtection(text) {
}

// Small wrapper to keep markdown behavior consistent in one place
const Markdown = ({ children, className }) => {
const Markdown = ({ children, className, dir }) => {
const content = normalizeInlineCodeFences(String(children ?? ''));
const remarkPlugins = useMemo(() => [remarkGfm, remarkMath], []);
const rehypePlugins = useMemo(() => [rehypeKatex], []);

return (
<div className={className}>
<div className={className} dir={dir}>
<ReactMarkdown
remarkPlugins={remarkPlugins}
rehypePlugins={rehypePlugins}
Expand Down Expand Up @@ -362,6 +363,12 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
(prevMessage.type === 'error'));
const messageRef = React.useRef(null);
const [isExpanded, setIsExpanded] = React.useState(false);

// Detect RTL characters in message content
const isRTL = React.useMemo(() => {
if (!message?.content) return false;
return hasRTLCharacters(message.content);
}, [message?.content]);
React.useEffect(() => {
if (!autoExpandTools || !messageRef.current || !message.isToolUse) return;

Expand Down Expand Up @@ -399,7 +406,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
/* User message bubble on the right */
<div className="flex items-end space-x-0 sm:space-x-3 w-full sm:w-auto sm:max-w-[85%] md:max-w-md lg:max-w-lg xl:max-w-xl">
<div className="bg-blue-600 text-white rounded-2xl rounded-br-md px-3 sm:px-4 py-2 shadow-sm flex-1 sm:flex-initial">
<div className="text-sm whitespace-pre-wrap break-words">
<div className="text-sm whitespace-pre-wrap break-words" dir={isRTL ? 'rtl' : 'ltr'}>
{message.content}
</div>
{message.images && message.images.length > 0 && (
Expand Down Expand Up @@ -1573,11 +1580,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile

// Normal rendering for non-JSON content
return message.type === 'assistant' ? (
<Markdown className="prose prose-sm max-w-none dark:prose-invert prose-gray">
<Markdown className="prose prose-sm max-w-none dark:prose-invert prose-gray" dir={isRTL ? 'rtl' : 'ltr'}>
{content}
</Markdown>
) : (
<div className="whitespace-pre-wrap">
<div className="whitespace-pre-wrap" dir={isRTL ? 'rtl' : 'ltr'}>
{content}
</div>
);
Expand Down Expand Up @@ -1648,6 +1655,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}
return '';
});

// Detect RTL characters in input field
const isInputRTL = useMemo(() => hasRTLCharacters(input), [input]);

const [chatMessages, setChatMessages] = useState(() => {
if (typeof window !== 'undefined' && selectedProject) {
const saved = safeLocalStorage.getItem(`chat_messages_${selectedProject.name}`);
Expand Down Expand Up @@ -4717,6 +4728,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}}
placeholder={`Type / for commands, @ for files, or ask ${provider === 'cursor' ? 'Cursor' : 'Claude'} anything...`}
disabled={isLoading}
dir={isInputRTL ? 'rtl' : 'ltr'}
className="chat-input-placeholder block w-full pl-12 pr-20 sm:pr-40 py-1.5 sm:py-4 bg-transparent rounded-2xl focus:outline-none text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 disabled:opacity-50 resize-none min-h-[50px] sm:min-h-[80px] max-h-[40vh] sm:max-h-[300px] overflow-y-auto text-sm sm:text-base leading-[21px] sm:leading-6 transition-all duration-200"
style={{ height: '50px' }}
/>
Expand Down
24 changes: 24 additions & 0 deletions src/utils/rtlDetection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Detects if text contains at least one RTL (right-to-left) character
* Checks for Arabic, Hebrew, and other RTL script characters
*
* @param {string} text - The text to analyze
* @returns {boolean} - True if at least one RTL character is found
*/
export function hasRTLCharacters(text) {
if (!text || typeof text !== 'string') {
return false;
}

// RTL Unicode ranges:
// Arabic: U+0600-U+06FF
// Hebrew: U+0590-U+05FF
// Arabic Supplement: U+0750-U+077F
// Arabic Extended-A: U+08A0-U+08FF
// Arabic Presentation Forms-A: U+FB50-U+FDFF
// Arabic Presentation Forms-B: U+FE70-U+FEFF
// Hebrew Presentation Forms: U+FB1D-U+FB4F
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB1D-\uFB4F\uFB50-\uFDFF\uFE70-\uFEFF]/;

return rtlRegex.test(text);
}