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]
Comment on lines +1 to +4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Usage comment is incomplete.

The usage comment only mentions start|stop but the script supports start|stop|restart|status|logs.

🔎 Proposed fix
 # Claude Code UI Control Script
-# Usage: ccui.sh [start|stop]
+# Usage: ccui.sh [start|stop|restart|status|logs]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#!/bin/bash
# Claude Code UI Control Script
# Usage: ccui.sh [start|stop]
#!/bin/bash
# Claude Code UI Control Script
# Usage: ccui.sh [start|stop|restart|status|logs]
🤖 Prompt for AI Agents
In ccui.sh around lines 1 to 4, the usage comment only lists "start|stop" but
the script actually supports "start|stop|restart|status|logs"; update the usage
comment to enumerate all supported commands (start|stop|restart|status|logs) and
optionally add a short example or brief note about required permissions so users
know how to invoke each action correctly.


PROJECT_DIR="/Users/alexsuprun/Documents/my-code/claudecodeui"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hardcoded path prevents portability.

The PROJECT_DIR is hardcoded to a specific user's machine path. This will break for any other user of the repository.

🔎 Proposed fix: Use script location for portability
-PROJECT_DIR="/Users/alexsuprun/Documents/my-code/claudecodeui"
+# Resolve project directory from script location
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$SCRIPT_DIR"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PROJECT_DIR="/Users/alexsuprun/Documents/my-code/claudecodeui"
# Resolve project directory from script location
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$SCRIPT_DIR"
🤖 Prompt for AI Agents
In ccui.sh around line 6, PROJECT_DIR is hardcoded to a single user path which
breaks portability; replace that hardcoded value with logic that derives the
project directory from the script's location (resolve the script path using
dirname/realpath or ${BASH_SOURCE[0]} as a fallback) so PROJECT_DIR points to
the repository root relative to the script and works for any user.

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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle cd failure to avoid running commands in wrong directory.

If the directory doesn't exist or is inaccessible, the script will continue running npm run dev from the wrong location.

🔎 Proposed fix
-    cd "$PROJECT_DIR"
+    cd "$PROJECT_DIR" || { echo "❌ Failed to change to project directory: $PROJECT_DIR"; exit 1; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cd "$PROJECT_DIR"
cd "$PROJECT_DIR" || { echo "❌ Failed to change to project directory: $PROJECT_DIR"; exit 1; }
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 46-46: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

(SC2164)

🤖 Prompt for AI Agents
In ccui.sh around line 46, the script does `cd "$PROJECT_DIR"` without checking
if it succeeded; add an explicit check immediately after the cd to detect
failure and stop the script (print a clear error to stderr and exit with a
non‑zero status) so subsequent commands like `npm run dev` aren’t executed from
the wrong directory; alternatively use a safe form that combines cd with an
immediate fallback exit (or enable errexit) to ensure the script aborts on cd
failure.

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);
}