diff --git a/.gitignore b/.gitignore
index bb65f13..9df63b3 100755
--- a/.gitignore
+++ b/.gitignore
@@ -98,10 +98,31 @@ temp/
# Local Netlify folder
.netlify
-# Claude specific
+# AI specific
.claude/
+.cursor/
+.roo/
+.taskmaster/
+.cline/
+.windsurf/
# Database files
*.db
*.sqlite
-*.sqlite3
\ No newline at end of file
+*.sqlite3
+
+logs
+dev-debug.log
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+# OS specific
+
+# Task files
+tasks.json
+tasks/
diff --git a/README.md b/README.md
index ad33afe..fcb505c 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for AI-assisted coding. You can use it locally or remotely to view your active projects and sessions in claude code and make changes to them the same way you would do it in claude code CLI. This gives you a proper interface that works everywhere.
+A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), and [Cursor CLI](https://docs.cursor.com/en/cli/overview). You can use it locally or remotely to view your active projects and sessions in Claude Code or Cursor and make changes to them from everywhere (mobile or desktop). This gives you a proper interface that works everywhere. Supports models including **Claude Sonnet 4**, **Opus 4.1**, and **GPT-5**
## Screenshots
@@ -25,6 +25,14 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
Responsive mobile design with touch navigation
+
+ {/* Thinking accordion for reasoning */}
+ {message.reasoning && (
+
+
+ 💭 Thinking...
+
+
+
+ {message.reasoning}
+
+
+
+ )}
+
{message.type === 'assistant' ? (
{
+ return localStorage.getItem('selected-provider') || 'claude';
+ });
+ const [cursorModel, setCursorModel] = useState(() => {
+ return localStorage.getItem('cursor-model') || 'gpt-5';
+ });
+ // When selecting a session from Sidebar, auto-switch provider to match session's origin
+ useEffect(() => {
+ if (selectedSession && selectedSession.__provider && selectedSession.__provider !== provider) {
+ setProvider(selectedSession.__provider);
+ localStorage.setItem('selected-provider', selectedSession.__provider);
+ }
+ }, [selectedSession]);
+
+ // Load Cursor default model from config
+ useEffect(() => {
+ if (provider === 'cursor') {
+ fetch('/api/cursor/config', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('auth-token')}`
+ }
+ })
+ .then(res => res.json())
+ .then(data => {
+ if (data.success && data.config?.model?.modelId) {
+ // Map Cursor model IDs to our simplified names
+ const modelMap = {
+ 'gpt-5': 'gpt-5',
+ 'claude-4-sonnet': 'sonnet-4',
+ 'sonnet-4': 'sonnet-4',
+ 'claude-4-opus': 'opus-4.1',
+ 'opus-4.1': 'opus-4.1'
+ };
+ const mappedModel = modelMap[data.config.model.modelId] || data.config.model.modelId;
+ if (!localStorage.getItem('cursor-model')) {
+ setCursorModel(mappedModel);
+ }
+ }
+ })
+ .catch(err => console.error('Error loading Cursor config:', err));
+ }
+ }, [provider]);
// Memoized diff calculation to prevent recalculating on every render
@@ -1164,21 +1235,356 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
};
}, []);
- // Load session messages from API
- const loadSessionMessages = useCallback(async (projectName, sessionId) => {
+ // Load session messages from API with pagination
+ const loadSessionMessages = useCallback(async (projectName, sessionId, loadMore = false) => {
if (!projectName || !sessionId) return [];
- setIsLoadingSessionMessages(true);
+ const isInitialLoad = !loadMore;
+ if (isInitialLoad) {
+ setIsLoadingSessionMessages(true);
+ } else {
+ setIsLoadingMoreMessages(true);
+ }
+
try {
- const response = await api.sessionMessages(projectName, sessionId);
+ const currentOffset = loadMore ? messagesOffset : 0;
+ const response = await api.sessionMessages(projectName, sessionId, MESSAGES_PER_PAGE, currentOffset);
if (!response.ok) {
throw new Error('Failed to load session messages');
}
const data = await response.json();
- return data.messages || [];
+
+ // Handle paginated response
+ if (data.hasMore !== undefined) {
+ setHasMoreMessages(data.hasMore);
+ setTotalMessages(data.total);
+ setMessagesOffset(currentOffset + (data.messages?.length || 0));
+ return data.messages || [];
+ } else {
+ // Backward compatibility for non-paginated response
+ const messages = data.messages || [];
+ setHasMoreMessages(false);
+ setTotalMessages(messages.length);
+ return messages;
+ }
} catch (error) {
console.error('Error loading session messages:', error);
return [];
+ } finally {
+ if (isInitialLoad) {
+ setIsLoadingSessionMessages(false);
+ } else {
+ setIsLoadingMoreMessages(false);
+ }
+ }
+ }, [messagesOffset]);
+
+ // Load Cursor session messages from SQLite via backend
+ const loadCursorSessionMessages = useCallback(async (projectPath, sessionId) => {
+ if (!projectPath || !sessionId) return [];
+ setIsLoadingSessionMessages(true);
+ try {
+ const url = `/api/cursor/sessions/${encodeURIComponent(sessionId)}?projectPath=${encodeURIComponent(projectPath)}`;
+ const res = await authenticatedFetch(url);
+ if (!res.ok) return [];
+ const data = await res.json();
+ const blobs = data?.session?.messages || [];
+ const converted = [];
+ const toolUseMap = {}; // Map to store tool uses by ID for linking results
+
+ // First pass: process all messages maintaining order
+ for (let blobIdx = 0; blobIdx < blobs.length; blobIdx++) {
+ const blob = blobs[blobIdx];
+ const content = blob.content;
+ let text = '';
+ let role = 'assistant';
+ let reasoningText = null; // Move to outer scope
+ try {
+ // Handle different Cursor message formats
+ if (content?.role && content?.content) {
+ // Direct format: {"role":"user","content":[{"type":"text","text":"..."}]}
+ // Skip system messages
+ if (content.role === 'system') {
+ continue;
+ }
+
+ // Handle tool messages
+ if (content.role === 'tool') {
+ // Tool result format - find the matching tool use message and update it
+ if (Array.isArray(content.content)) {
+ for (const item of content.content) {
+ if (item?.type === 'tool-result') {
+ // Map ApplyPatch to Edit for consistency
+ let toolName = item.toolName || 'Unknown Tool';
+ if (toolName === 'ApplyPatch') {
+ toolName = 'Edit';
+ }
+ const toolCallId = item.toolCallId || content.id;
+ const result = item.result || '';
+
+ // Store the tool result to be linked later
+ if (toolUseMap[toolCallId]) {
+ toolUseMap[toolCallId].toolResult = {
+ content: result,
+ isError: false
+ };
+ } else {
+ // No matching tool use found, create a standalone result message
+ converted.push({
+ type: 'assistant',
+ content: '',
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid,
+ isToolUse: true,
+ toolName: toolName,
+ toolId: toolCallId,
+ toolInput: null,
+ toolResult: {
+ content: result,
+ isError: false
+ }
+ });
+ }
+ }
+ }
+ }
+ continue; // Don't add tool messages as regular messages
+ } else {
+ // User or assistant messages
+ role = content.role === 'user' ? 'user' : 'assistant';
+
+ if (Array.isArray(content.content)) {
+ // Extract text, reasoning, and tool calls from content array
+ const textParts = [];
+
+ for (const part of content.content) {
+ if (part?.type === 'text' && part?.text) {
+ textParts.push(part.text);
+ } else if (part?.type === 'reasoning' && part?.text) {
+ // Handle reasoning type - will be displayed in a collapsible section
+ reasoningText = part.text;
+ } else if (part?.type === 'tool-call') {
+ // First, add any text/reasoning we've collected so far as a message
+ if (textParts.length > 0 || reasoningText) {
+ converted.push({
+ type: role,
+ content: textParts.join('\n'),
+ reasoning: reasoningText,
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid
+ });
+ textParts.length = 0;
+ reasoningText = null;
+ }
+
+ // Tool call in assistant message - format like Claude Code
+ // Map ApplyPatch to Edit for consistency with Claude Code
+ let toolName = part.toolName || 'Unknown Tool';
+ if (toolName === 'ApplyPatch') {
+ toolName = 'Edit';
+ }
+ const toolId = part.toolCallId || `tool_${blobIdx}`;
+
+ // Create a tool use message with Claude Code format
+ // Map Cursor args format to Claude Code format
+ let toolInput = part.args;
+
+ if (toolName === 'Edit' && part.args) {
+ // ApplyPatch uses 'patch' format, convert to Edit format
+ if (part.args.patch) {
+ // Parse the patch to extract old and new content
+ const patchLines = part.args.patch.split('\n');
+ let oldLines = [];
+ let newLines = [];
+ let inPatch = false;
+
+ for (const line of patchLines) {
+ if (line.startsWith('@@')) {
+ inPatch = true;
+ } else if (inPatch) {
+ if (line.startsWith('-')) {
+ oldLines.push(line.substring(1));
+ } else if (line.startsWith('+')) {
+ newLines.push(line.substring(1));
+ } else if (line.startsWith(' ')) {
+ // Context line - add to both
+ oldLines.push(line.substring(1));
+ newLines.push(line.substring(1));
+ }
+ }
+ }
+
+ const filePath = part.args.file_path;
+ const absolutePath = filePath && !filePath.startsWith('/')
+ ? `${projectPath}/${filePath}`
+ : filePath;
+ toolInput = {
+ file_path: absolutePath,
+ old_string: oldLines.join('\n') || part.args.patch,
+ new_string: newLines.join('\n') || part.args.patch
+ };
+ } else {
+ // Direct edit format
+ toolInput = part.args;
+ }
+ } else if (toolName === 'Read' && part.args) {
+ // Map 'path' to 'file_path'
+ // Convert relative path to absolute if needed
+ const filePath = part.args.path || part.args.file_path;
+ const absolutePath = filePath && !filePath.startsWith('/')
+ ? `${projectPath}/${filePath}`
+ : filePath;
+ toolInput = {
+ file_path: absolutePath
+ };
+ } else if (toolName === 'Write' && part.args) {
+ // Map fields for Write tool
+ const filePath = part.args.path || part.args.file_path;
+ const absolutePath = filePath && !filePath.startsWith('/')
+ ? `${projectPath}/${filePath}`
+ : filePath;
+ toolInput = {
+ file_path: absolutePath,
+ content: part.args.contents || part.args.content
+ };
+ }
+
+ const toolMessage = {
+ type: 'assistant',
+ content: '',
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid,
+ isToolUse: true,
+ toolName: toolName,
+ toolId: toolId,
+ toolInput: toolInput ? JSON.stringify(toolInput) : null,
+ toolResult: null // Will be filled when we get the tool result
+ };
+ converted.push(toolMessage);
+ toolUseMap[toolId] = toolMessage; // Store for linking results
+ } else if (part?.type === 'tool_use') {
+ // Old format support
+ if (textParts.length > 0 || reasoningText) {
+ converted.push({
+ type: role,
+ content: textParts.join('\n'),
+ reasoning: reasoningText,
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid
+ });
+ textParts.length = 0;
+ reasoningText = null;
+ }
+
+ const toolName = part.name || 'Unknown Tool';
+ const toolId = part.id || `tool_${blobIdx}`;
+
+ const toolMessage = {
+ type: 'assistant',
+ content: '',
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid,
+ isToolUse: true,
+ toolName: toolName,
+ toolId: toolId,
+ toolInput: part.input ? JSON.stringify(part.input) : null,
+ toolResult: null
+ };
+ converted.push(toolMessage);
+ toolUseMap[toolId] = toolMessage;
+ } else if (typeof part === 'string') {
+ textParts.push(part);
+ }
+ }
+
+ // Add any remaining text/reasoning
+ if (textParts.length > 0) {
+ text = textParts.join('\n');
+ if (reasoningText && !text) {
+ // Just reasoning, no text
+ converted.push({
+ type: role,
+ content: '',
+ reasoning: reasoningText,
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid
+ });
+ text = ''; // Clear to avoid duplicate
+ }
+ } else {
+ text = '';
+ }
+ } else if (typeof content.content === 'string') {
+ text = content.content;
+ }
+ }
+ } else if (content?.message?.role && content?.message?.content) {
+ // Nested message format
+ if (content.message.role === 'system') {
+ continue;
+ }
+ role = content.message.role === 'user' ? 'user' : 'assistant';
+ if (Array.isArray(content.message.content)) {
+ text = content.message.content
+ .map(p => (typeof p === 'string' ? p : (p?.text || '')))
+ .filter(Boolean)
+ .join('\n');
+ } else if (typeof content.message.content === 'string') {
+ text = content.message.content;
+ }
+ }
+ } catch (e) {
+ console.log('Error parsing blob content:', e);
+ }
+ if (text && text.trim()) {
+ const message = {
+ type: role,
+ content: text,
+ timestamp: new Date(Date.now() + blobIdx * 1000),
+ blobId: blob.id,
+ sequence: blob.sequence,
+ rowid: blob.rowid
+ };
+
+ // Add reasoning if we have it
+ if (reasoningText) {
+ message.reasoning = reasoningText;
+ }
+
+ converted.push(message);
+ }
+ }
+
+ // Sort messages by sequence/rowid to maintain chronological order
+ converted.sort((a, b) => {
+ // First sort by sequence if available (clean 1,2,3... numbering)
+ if (a.sequence !== undefined && b.sequence !== undefined) {
+ return a.sequence - b.sequence;
+ }
+ // Then try rowid (original SQLite row IDs)
+ if (a.rowid !== undefined && b.rowid !== undefined) {
+ return a.rowid - b.rowid;
+ }
+ // Fallback to timestamp
+ return new Date(a.timestamp) - new Date(b.timestamp);
+ });
+
+ return converted;
+ } catch (e) {
+ console.error('Error loading Cursor session messages:', e);
+ return [];
} finally {
setIsLoadingSessionMessages(false);
}
@@ -1337,43 +1743,106 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
return scrollHeight - scrollTop - clientHeight < 50;
}, []);
- // Handle scroll events to detect when user manually scrolls up
- const handleScroll = useCallback(() => {
+ // Handle scroll events to detect when user manually scrolls up and load more messages
+ const handleScroll = useCallback(async () => {
if (scrollContainerRef.current) {
+ const container = scrollContainerRef.current;
const nearBottom = isNearBottom();
setIsUserScrolledUp(!nearBottom);
+
+ // Check if we should load more messages (scrolled near top)
+ const scrolledNearTop = container.scrollTop < 100;
+ const provider = localStorage.getItem('selected-provider') || 'claude';
+
+ if (scrolledNearTop && hasMoreMessages && !isLoadingMoreMessages && selectedSession && selectedProject && provider !== 'cursor') {
+ // Save current scroll position
+ const previousScrollHeight = container.scrollHeight;
+ const previousScrollTop = container.scrollTop;
+
+ // Load more messages
+ const moreMessages = await loadSessionMessages(selectedProject.name, selectedSession.id, true);
+
+ if (moreMessages.length > 0) {
+ // Prepend new messages to the existing ones
+ setSessionMessages(prev => [...moreMessages, ...prev]);
+
+ // Restore scroll position after DOM update
+ setTimeout(() => {
+ if (scrollContainerRef.current) {
+ const newScrollHeight = scrollContainerRef.current.scrollHeight;
+ const scrollDiff = newScrollHeight - previousScrollHeight;
+ scrollContainerRef.current.scrollTop = previousScrollTop + scrollDiff;
+ }
+ }, 0);
+ }
+ }
}
- }, [isNearBottom]);
+ }, [isNearBottom, hasMoreMessages, isLoadingMoreMessages, selectedSession, selectedProject, loadSessionMessages]);
useEffect(() => {
// Load session messages when session changes
const loadMessages = async () => {
if (selectedSession && selectedProject) {
- setCurrentSessionId(selectedSession.id);
+ const provider = localStorage.getItem('selected-provider') || 'claude';
- // Only load messages from API if this is a user-initiated session change
- // For system-initiated changes, preserve existing messages and rely on WebSocket
- if (!isSystemSessionChange) {
- const messages = await loadSessionMessages(selectedProject.name, selectedSession.id);
- setSessionMessages(messages);
- // convertedMessages will be automatically updated via useMemo
- // Scroll to bottom after loading session messages if auto-scroll is enabled
- if (autoScrollToBottom) {
- setTimeout(() => scrollToBottom(), 200);
+ // Reset pagination state when switching sessions
+ setMessagesOffset(0);
+ setHasMoreMessages(false);
+ setTotalMessages(0);
+
+ if (provider === 'cursor') {
+ // For Cursor, set the session ID for resuming
+ setCurrentSessionId(selectedSession.id);
+ sessionStorage.setItem('cursorSessionId', selectedSession.id);
+
+ // Only load messages from SQLite if this is NOT a system-initiated session change
+ // For system-initiated changes, preserve existing messages
+ if (!isSystemSessionChange) {
+ // Load historical messages for Cursor session from SQLite
+ const projectPath = selectedProject.fullPath || selectedProject.path;
+ const converted = await loadCursorSessionMessages(projectPath, selectedSession.id);
+ setSessionMessages([]);
+ setChatMessages(converted);
+ } else {
+ // Reset the flag after handling system session change
+ setIsSystemSessionChange(false);
}
} else {
- // Reset the flag after handling system session change
- setIsSystemSessionChange(false);
+ // For Claude, load messages normally with pagination
+ setCurrentSessionId(selectedSession.id);
+
+ // Only load messages from API if this is a user-initiated session change
+ // For system-initiated changes, preserve existing messages and rely on WebSocket
+ if (!isSystemSessionChange) {
+ const messages = await loadSessionMessages(selectedProject.name, selectedSession.id, false);
+ setSessionMessages(messages);
+ // convertedMessages will be automatically updated via useMemo
+ // Scroll to bottom after loading session messages if auto-scroll is enabled
+ if (autoScrollToBottom) {
+ setTimeout(() => scrollToBottom(), 200);
+ }
+ } else {
+ // Reset the flag after handling system session change
+ setIsSystemSessionChange(false);
+ }
}
} else {
- setChatMessages([]);
- setSessionMessages([]);
+ // Only clear messages if this is NOT a system-initiated session change AND we're not loading
+ // During system session changes or while loading, preserve the chat messages
+ if (!isSystemSessionChange && !isLoading) {
+ setChatMessages([]);
+ setSessionMessages([]);
+ }
setCurrentSessionId(null);
+ sessionStorage.removeItem('cursorSessionId');
+ setMessagesOffset(0);
+ setHasMoreMessages(false);
+ setTotalMessages(0);
}
};
loadMessages();
- }, [selectedSession, selectedProject, loadSessionMessages, scrollToBottom, isSystemSessionChange]);
+ }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]);
// Update chatMessages when convertedMessages changes
useEffect(() => {
@@ -1441,6 +1910,63 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
case 'claude-response':
const messageData = latestMessage.data.message || latestMessage.data;
+ // Handle Cursor streaming format (content_block_delta / content_block_stop)
+ if (messageData && typeof messageData === 'object' && messageData.type) {
+ if (messageData.type === 'content_block_delta' && messageData.delta?.text) {
+ // Buffer deltas and flush periodically to reduce rerenders
+ streamBufferRef.current += messageData.delta.text;
+ if (!streamTimerRef.current) {
+ streamTimerRef.current = setTimeout(() => {
+ const chunk = streamBufferRef.current;
+ streamBufferRef.current = '';
+ streamTimerRef.current = null;
+ if (!chunk) return;
+ setChatMessages(prev => {
+ const updated = [...prev];
+ const last = updated[updated.length - 1];
+ if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
+ last.content = (last.content || '') + chunk;
+ } else {
+ updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
+ }
+ return updated;
+ });
+ }, 100);
+ }
+ return;
+ }
+ if (messageData.type === 'content_block_stop') {
+ // Flush any buffered text and mark streaming message complete
+ if (streamTimerRef.current) {
+ clearTimeout(streamTimerRef.current);
+ streamTimerRef.current = null;
+ }
+ const chunk = streamBufferRef.current;
+ streamBufferRef.current = '';
+ if (chunk) {
+ setChatMessages(prev => {
+ const updated = [...prev];
+ const last = updated[updated.length - 1];
+ if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
+ last.content = (last.content || '') + chunk;
+ } else {
+ updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
+ }
+ return updated;
+ });
+ }
+ setChatMessages(prev => {
+ const updated = [...prev];
+ const last = updated[updated.length - 1];
+ if (last && last.type === 'assistant' && last.isStreaming) {
+ last.isStreaming = false;
+ }
+ return updated;
+ });
+ return;
+ }
+ }
+
// Handle Claude CLI session duplication bug workaround:
// When resuming a session, Claude CLI creates a new session instead of resuming.
// We detect this by checking for system/init messages with session_id that differs
@@ -1581,11 +2107,30 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
break;
case 'claude-output':
- setChatMessages(prev => [...prev, {
- type: 'assistant',
- content: latestMessage.data,
- timestamp: new Date()
- }]);
+ {
+ const cleaned = String(latestMessage.data || '');
+ if (cleaned.trim()) {
+ streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned);
+ if (!streamTimerRef.current) {
+ streamTimerRef.current = setTimeout(() => {
+ const chunk = streamBufferRef.current;
+ streamBufferRef.current = '';
+ streamTimerRef.current = null;
+ if (!chunk) return;
+ setChatMessages(prev => {
+ const updated = [...prev];
+ const last = updated[updated.length - 1];
+ if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
+ last.content = last.content ? `${last.content}\n${chunk}` : chunk;
+ } else {
+ updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
+ }
+ return updated;
+ });
+ }, 100);
+ }
+ }
+ }
break;
case 'claude-interactive-prompt':
// Handle interactive prompts from CLI
@@ -1605,6 +2150,145 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}]);
break;
+ case 'cursor-system':
+ // Handle Cursor system/init messages similar to Claude
+ try {
+ const cdata = latestMessage.data;
+ if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) {
+ // If we already have a session and this differs, switch (duplication/redirect)
+ if (currentSessionId && cdata.session_id !== currentSessionId) {
+ console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id });
+ setIsSystemSessionChange(true);
+ if (onNavigateToSession) {
+ onNavigateToSession(cdata.session_id);
+ }
+ return;
+ }
+ // If we don't yet have a session, adopt this one
+ if (!currentSessionId) {
+ console.log('🔄 Cursor new session init detected:', { newSession: cdata.session_id });
+ setIsSystemSessionChange(true);
+ if (onNavigateToSession) {
+ onNavigateToSession(cdata.session_id);
+ }
+ return;
+ }
+ }
+ // For other cursor-system messages, avoid dumping raw objects to chat
+ } catch (e) {
+ console.warn('Error handling cursor-system message:', e);
+ }
+ break;
+
+ case 'cursor-user':
+ // Handle Cursor user messages (usually echoes)
+ // Don't add user messages as they're already shown from input
+ break;
+
+ case 'cursor-tool-use':
+ // Handle Cursor tool use messages
+ setChatMessages(prev => [...prev, {
+ type: 'assistant',
+ content: `Using tool: ${latestMessage.tool} ${latestMessage.input ? `with ${latestMessage.input}` : ''}`,
+ timestamp: new Date(),
+ isToolUse: true,
+ toolName: latestMessage.tool,
+ toolInput: latestMessage.input
+ }]);
+ break;
+
+ case 'cursor-error':
+ // Show Cursor errors as error messages in chat
+ setChatMessages(prev => [...prev, {
+ type: 'error',
+ content: `Cursor error: ${latestMessage.error || 'Unknown error'}`,
+ timestamp: new Date()
+ }]);
+ break;
+
+ case 'cursor-result':
+ // Handle Cursor completion and final result text
+ setIsLoading(false);
+ setCanAbortSession(false);
+ setClaudeStatus(null);
+ try {
+ const r = latestMessage.data || {};
+ const textResult = typeof r.result === 'string' ? r.result : '';
+ // Flush buffered deltas before finalizing
+ if (streamTimerRef.current) {
+ clearTimeout(streamTimerRef.current);
+ streamTimerRef.current = null;
+ }
+ const pendingChunk = streamBufferRef.current;
+ streamBufferRef.current = '';
+
+ setChatMessages(prev => {
+ const updated = [...prev];
+ // Try to consolidate into the last streaming assistant message
+ const last = updated[updated.length - 1];
+ if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
+ // Replace streaming content with the final content so deltas don't remain
+ const finalContent = textResult && textResult.trim() ? textResult : (last.content || '') + (pendingChunk || '');
+ last.content = finalContent;
+ last.isStreaming = false;
+ } else if (textResult && textResult.trim()) {
+ updated.push({ type: r.is_error ? 'error' : 'assistant', content: textResult, timestamp: new Date(), isStreaming: false });
+ }
+ return updated;
+ });
+ } catch (e) {
+ console.warn('Error handling cursor-result message:', e);
+ }
+
+ // Mark session as inactive
+ const cursorSessionId = currentSessionId || sessionStorage.getItem('pendingSessionId');
+ if (cursorSessionId && onSessionInactive) {
+ onSessionInactive(cursorSessionId);
+ }
+
+ // Store session ID for future use and trigger refresh
+ if (cursorSessionId && !currentSessionId) {
+ setCurrentSessionId(cursorSessionId);
+ sessionStorage.removeItem('pendingSessionId');
+
+ // Trigger a project refresh to update the sidebar with the new session
+ if (window.refreshProjects) {
+ setTimeout(() => window.refreshProjects(), 500);
+ }
+ }
+ break;
+
+ case 'cursor-output':
+ // Handle Cursor raw terminal output; strip ANSI and ignore empty control-only payloads
+ try {
+ const raw = String(latestMessage.data ?? '');
+ const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim();
+ if (cleaned) {
+ streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned);
+ if (!streamTimerRef.current) {
+ streamTimerRef.current = setTimeout(() => {
+ const chunk = streamBufferRef.current;
+ streamBufferRef.current = '';
+ streamTimerRef.current = null;
+ if (!chunk) return;
+ setChatMessages(prev => {
+ const updated = [...prev];
+ const last = updated[updated.length - 1];
+ if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) {
+ last.content = last.content ? `${last.content}\n${chunk}` : chunk;
+ } else {
+ updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true });
+ }
+ return updated;
+ });
+ }, 100);
+ }
+ }
+ } catch (e) {
+ console.warn('Error handling cursor-output message:', e);
+ }
+ break;
+
case 'claude-complete':
setIsLoading(false);
setCanAbortSession(false);
@@ -1624,6 +2308,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
if (pendingSessionId && !currentSessionId && latestMessage.exitCode === 0) {
setCurrentSessionId(pendingSessionId);
sessionStorage.removeItem('pendingSessionId');
+
+ // Trigger a project refresh to update the sidebar with the new session
+ if (window.refreshProjects) {
+ setTimeout(() => window.refreshProjects(), 500);
+ }
}
// Clear persisted chat messages after successful completion
@@ -1652,7 +2341,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
case 'claude-status':
// Handle Claude working status messages
- console.log('🔔 Received claude-status message:', latestMessage);
const statusData = latestMessage.data;
if (statusData) {
// Parse the status message to extract relevant information
@@ -1683,7 +2371,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
statusInfo.can_interrupt = statusData.can_interrupt;
}
- console.log('📊 Setting claude status:', statusInfo);
setClaudeStatus(statusInfo);
setIsLoading(true);
setCanAbortSession(statusInfo.can_interrupt);
@@ -2017,20 +2704,21 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response
setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered
+ // Determine effective session id for replies to avoid race on state updates
+ const effectiveSessionId = currentSessionId || selectedSession?.id || sessionStorage.getItem('cursorSessionId');
+
// Session Protection: Mark session as active to prevent automatic project updates during conversation
- // This is crucial for maintaining chat state integrity. We handle two cases:
- // 1. Existing sessions: Use the real currentSessionId
- // 2. New sessions: Generate temporary identifier "new-session-{timestamp}" since real ID comes via WebSocket later
- // This ensures no gap in protection between message send and session creation
- const sessionToActivate = currentSessionId || `new-session-${Date.now()}`;
+ // Use existing session if available; otherwise a temporary placeholder until backend provides real ID
+ const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`;
if (onSessionActive) {
onSessionActive(sessionToActivate);
}
- // Get tools settings from localStorage
+ // Get tools settings from localStorage based on provider
const getToolsSettings = () => {
try {
- const savedSettings = safeLocalStorage.getItem('claude-tools-settings');
+ const settingsKey = provider === 'cursor' ? 'cursor-tools-settings' : 'claude-tools-settings';
+ const savedSettings = safeLocalStorage.getItem(settingsKey);
if (savedSettings) {
return JSON.parse(savedSettings);
}
@@ -2046,20 +2734,40 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
const toolsSettings = getToolsSettings();
- // Send command to Claude CLI via WebSocket with images
- sendMessage({
- type: 'claude-command',
- command: input,
- options: {
- projectPath: selectedProject.path,
- cwd: selectedProject.fullPath,
- sessionId: currentSessionId,
- resume: !!currentSessionId,
- toolsSettings: toolsSettings,
- permissionMode: permissionMode,
- images: uploadedImages // Pass images to backend
- }
- });
+ // Send command based on provider
+ if (provider === 'cursor') {
+ // Send Cursor command (always use cursor-command; include resume/sessionId when replying)
+ sendMessage({
+ type: 'cursor-command',
+ command: input,
+ sessionId: effectiveSessionId,
+ options: {
+ // Prefer fullPath (actual cwd for project), fallback to path
+ cwd: selectedProject.fullPath || selectedProject.path,
+ projectPath: selectedProject.fullPath || selectedProject.path,
+ sessionId: effectiveSessionId,
+ resume: !!effectiveSessionId,
+ model: cursorModel,
+ skipPermissions: toolsSettings?.skipPermissions || false,
+ toolsSettings: toolsSettings
+ }
+ });
+ } else {
+ // Send Claude command (existing code)
+ sendMessage({
+ type: 'claude-command',
+ command: input,
+ options: {
+ projectPath: selectedProject.path,
+ cwd: selectedProject.fullPath,
+ sessionId: currentSessionId,
+ resume: !!currentSessionId,
+ toolsSettings: toolsSettings,
+ permissionMode: permissionMode,
+ images: uploadedImages // Pass images to backend
+ }
+ });
+ }
setInput('');
setAttachedImages([]);
@@ -2211,7 +2919,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
if (currentSessionId && canAbortSession) {
sendMessage({
type: 'abort-session',
- sessionId: currentSessionId
+ sessionId: currentSessionId,
+ provider: provider
});
}
};
@@ -2258,16 +2967,145 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
) : chatMessages.length === 0 ? (
-
-
Start a conversation with Claude
-
- Ask questions about your code, request changes, or get help with development tasks
-
-
+ {!selectedSession && !currentSessionId && (
+
+
Choose Your AI Assistant
+
+ Select a provider to start a new conversation
+
+
+
+ {/* Claude Button */}
+
{
+ setProvider('claude');
+ localStorage.setItem('selected-provider', 'claude');
+ // Focus input after selection
+ setTimeout(() => textareaRef.current?.focus(), 100);
+ }}
+ className={`group relative w-64 h-32 bg-white dark:bg-gray-800 rounded-xl border-2 transition-all duration-200 hover:scale-105 hover:shadow-xl ${
+ provider === 'claude'
+ ? 'border-blue-500 shadow-lg ring-2 ring-blue-500/20'
+ : 'border-gray-200 dark:border-gray-700 hover:border-blue-400'
+ }`}
+ >
+
+
+
+
Claude
+
by Anthropic
+
+
+ {provider === 'claude' && (
+
+ )}
+
+
+ {/* Cursor Button */}
+
{
+ setProvider('cursor');
+ localStorage.setItem('selected-provider', 'cursor');
+ // Focus input after selection
+ setTimeout(() => textareaRef.current?.focus(), 100);
+ }}
+ className={`group relative w-64 h-32 bg-white dark:bg-gray-800 rounded-xl border-2 transition-all duration-200 hover:scale-105 hover:shadow-xl ${
+ provider === 'cursor'
+ ? 'border-purple-500 shadow-lg ring-2 ring-purple-500/20'
+ : 'border-gray-200 dark:border-gray-700 hover:border-purple-400'
+ }`}
+ >
+
+
+
+
Cursor
+
AI Code Editor
+
+
+ {provider === 'cursor' && (
+
+ )}
+
+
+
+ {/* Model Selection for Cursor - Always reserve space to prevent jumping */}
+
+
+ {provider === 'cursor' ? 'Select Model' : '\u00A0'}
+
+ {
+ const newModel = e.target.value;
+ setCursorModel(newModel);
+ localStorage.setItem('cursor-model', newModel);
+ }}
+ className="pl-4 pr-10 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 min-w-[140px]"
+ disabled={provider !== 'cursor'}
+ >
+ GPT-5
+ Sonnet-4
+ Opus 4.1
+
+
+
+
+ {provider === 'claude'
+ ? 'Ready to use Claude AI. Start typing your message below.'
+ : provider === 'cursor'
+ ? `Ready to use Cursor with ${cursorModel}. Start typing your message below.`
+ : 'Select a provider above to begin'
+ }
+
+
+ )}
+ {selectedSession && (
+
+
Continue your conversation
+
+ Ask questions about your code, request changes, or get help with development tasks
+
+
+ )}
) : (
<>
- {chatMessages.length > visibleMessageCount && (
+ {/* Loading indicator for older messages */}
+ {isLoadingMoreMessages && (
+
+
+
+
Loading older messages...
+
+
+ )}
+
+ {/* Indicator showing there are more messages to load */}
+ {hasMoreMessages && !isLoadingMoreMessages && (
+
+ {totalMessages > 0 && (
+
+ Showing {sessionMessages.length} of {totalMessages} messages •
+ Scroll up to load more
+
+ )}
+
+ )}
+
+ {/* Legacy message count indicator (for non-paginated view) */}
+ {!hasMoreMessages && chatMessages.length > visibleMessageCount && (
Showing last {visibleMessageCount} messages ({chatMessages.length} total) •
-
- C
+
+ {(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? (
+
+ ) : (
+
+ )}
-
Claude
+
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude'}
{/* Abort button removed - functionality not yet implemented at backend */}
@@ -2326,16 +3168,18 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
{/* Input Area - Fixed Bottom */}
-
- {/* Claude Working Status - positioned above the input form */}
-
-
+
+
+
+
{/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
diff --git a/src/components/ClaudeStatus.jsx b/src/components/ClaudeStatus.jsx
index 52bf39d..c19e37b 100644
--- a/src/components/ClaudeStatus.jsx
+++ b/src/components/ClaudeStatus.jsx
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { cn } from '../lib/utils';
-function ClaudeStatus({ status, onAbort, isLoading }) {
+function ClaudeStatus({ status, onAbort, isLoading, provider = 'claude' }) {
const [elapsedTime, setElapsedTime] = useState(0);
const [animationPhase, setAnimationPhase] = useState(0);
const [fakeTokens, setFakeTokens] = useState(0);
diff --git a/src/components/CursorLogo.jsx b/src/components/CursorLogo.jsx
new file mode 100644
index 0000000..18bda9d
--- /dev/null
+++ b/src/components/CursorLogo.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const CursorLogo = ({ className = 'w-5 h-5' }) => {
+ return (
+
+ );
+};
+
+export default CursorLogo;
diff --git a/src/components/GitPanel.jsx b/src/components/GitPanel.jsx
index 06d6217..b71d2ef 100644
--- a/src/components/GitPanel.jsx
+++ b/src/components/GitPanel.jsx
@@ -60,14 +60,12 @@ function GitPanel({ selectedProject, isMobile }) {
const fetchGitStatus = async () => {
if (!selectedProject) return;
- console.log('Fetching git status for project:', selectedProject.name, 'path:', selectedProject.path);
setIsLoading(true);
try {
const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
const data = await response.json();
- console.log('Git status response:', data);
if (data.error) {
console.error('Git status error:', data.error);
diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx
index 52dc236..09a210a 100644
--- a/src/components/MainContent.jsx
+++ b/src/components/MainContent.jsx
@@ -18,6 +18,8 @@ import CodeEditor from './CodeEditor';
import Shell from './Shell';
import GitPanel from './GitPanel';
import ErrorBoundary from './ErrorBoundary';
+import ClaudeLogo from './ClaudeLogo';
+import CursorLogo from './CursorLogo';
function MainContent({
selectedProject,
@@ -153,35 +155,46 @@ function MainContent({
)}
-
- {activeTab === 'chat' && selectedSession ? (
-
-
- {selectedSession.summary}
-
-
- {selectedProject.displayName} • {selectedSession.id}
-
-
- ) : activeTab === 'chat' && !selectedSession ? (
-
-
- New Session
-
-
- {selectedProject.displayName}
-
-
- ) : (
-
-
- {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
-
-
- {selectedProject.displayName}
-
+
+ {activeTab === 'chat' && selectedSession && (
+
+ {selectedSession.__provider === 'cursor' ? (
+
+ ) : (
+
+ )}
)}
+
+ {activeTab === 'chat' && selectedSession ? (
+
+
+ {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')}
+
+
+ {selectedProject.displayName} • {selectedSession.id}
+
+
+ ) : activeTab === 'chat' && !selectedSession ? (
+
+
+ New Session
+
+
+ {selectedProject.displayName}
+
+
+ ) : (
+
+
+ {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'}
+
+
+ {selectedProject.displayName}
+
+
+ )}
+
diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx
index 3033b03..03d2bfd 100644
--- a/src/components/Shell.jsx
+++ b/src/components/Shell.jsx
@@ -436,6 +436,7 @@ function Shell({ selectedProject, selectedSession, isActive }) {
projectPath: selectedProject.fullPath || selectedProject.path,
sessionId: selectedSession?.id,
hasSession: !!selectedSession,
+ provider: selectedSession?.__provider || 'claude',
cols: terminal.current.cols,
rows: terminal.current.rows
};
@@ -530,11 +531,16 @@ function Shell({ selectedProject, selectedSession, isActive }) {
- {selectedSession && (
-
- ({selectedSession.summary.slice(0, 30)}...)
-
- )}
+ {selectedSession && (() => {
+ const displaySessionName = selectedSession.__provider === 'cursor'
+ ? (selectedSession.name || 'Untitled Session')
+ : (selectedSession.summary || 'New Session');
+ return (
+
+ ({displaySessionName.slice(0, 30)}...)
+
+ );
+ })()}
{!selectedSession && (
(New Session)
)}
@@ -601,7 +607,12 @@ function Shell({ selectedProject, selectedSession, isActive }) {
{selectedSession ?
- `Resume session: ${selectedSession.summary.slice(0, 50)}...` :
+ (() => {
+ const displaySessionName = selectedSession.__provider === 'cursor'
+ ? (selectedSession.name || 'Untitled Session')
+ : (selectedSession.summary || 'New Session');
+ return `Resume session: ${displaySessionName.slice(0, 50)}...`;
+ })() :
'Start a new Claude session'
}
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx
index 9a1e111..36f3bbf 100644
--- a/src/components/Sidebar.jsx
+++ b/src/components/Sidebar.jsx
@@ -7,6 +7,7 @@ import { Input } from './ui/input';
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react';
import { cn } from '../lib/utils';
import ClaudeLogo from './ClaudeLogo';
+import CursorLogo from './CursorLogo.jsx';
import { api } from '../utils/api';
// Move formatTimeAgo outside component to avoid recreation on every render
@@ -202,9 +203,12 @@ function Sidebar({
// Helper function to get all sessions for a project (initial + additional)
const getAllSessions = (project) => {
- const initialSessions = project.sessions || [];
- const additional = additionalSessions[project.name] || [];
- return [...initialSessions, ...additional];
+ // Combine Claude and Cursor sessions; Sidebar will display icon per row
+ const claudeSessions = [...(project.sessions || []), ...(additionalSessions[project.name] || [])].map(s => ({ ...s, __provider: 'claude' }));
+ const cursorSessions = (project.cursorSessions || []).map(s => ({ ...s, __provider: 'cursor' }));
+ // Sort by most recent activity/date
+ const normalizeDate = (s) => new Date(s.__provider === 'cursor' ? s.createdAt : s.lastActivity);
+ return [...claudeSessions, ...cursorSessions].sort((a, b) => normalizeDate(b) - normalizeDate(a));
};
// Helper function to get the last activity date for a project
@@ -979,11 +983,19 @@ function Sidebar({
) : (
getAllSessions(project).map((session) => {
+ // Handle both Claude and Cursor session formats
+ const isCursorSession = session.__provider === 'cursor';
+
// Calculate if session is active (within last 10 minutes)
- const sessionDate = new Date(session.lastActivity);
+ const sessionDate = new Date(isCursorSession ? session.createdAt : session.lastActivity);
const diffInMinutes = Math.floor((currentTime - sessionDate) / (1000 * 60));
const isActive = diffInMinutes < 10;
+ // Get session display values
+ const sessionName = isCursorSession ? (session.name || 'Untitled Session') : (session.summary || 'New Session');
+ const sessionTime = isCursorSession ? session.createdAt : session.lastActivity;
+ const messageCount = session.messageCount || 0;
+
return (
{/* Active session indicator dot */}
@@ -1014,38 +1026,49 @@ function Sidebar({
"w-5 h-5 rounded-md flex items-center justify-center flex-shrink-0",
selectedSession?.id === session.id ? "bg-primary/10" : "bg-muted/50"
)}>
-
+ {isCursorSession ? (
+
+ ) : (
+
+ )}
- {session.summary || 'New Session'}
+ {sessionName}
-
+
- {formatTimeAgo(session.lastActivity, currentTime)}
+ {formatTimeAgo(sessionTime, currentTime)}
- {session.messageCount > 0 && (
+ {messageCount > 0 && (
- {session.messageCount}
+ {messageCount}
)}
+ {/* Provider tiny icon */}
+
+ {isCursorSession ? (
+
+ ) : (
+
+ )}
+
- {/* Mobile delete button */}
-
{
- e.stopPropagation();
- deleteSession(project.name, session.id);
- }}
- onTouchEnd={handleTouchClick(() => deleteSession(project.name, session.id))}
- >
-
-
+ {/* Mobile delete button - only for Claude sessions */}
+ {!isCursorSession && (
+
{
+ e.stopPropagation();
+ deleteSession(project.name, session.id);
+ }}
+ onTouchEnd={handleTouchClick(() => deleteSession(project.name, session.id))}
+ >
+
+
+ )}
@@ -1062,26 +1085,39 @@ function Sidebar({
onTouchEnd={handleTouchClick(() => onSessionSelect(session))}
>
-
+ {isCursorSession ? (
+
+ ) : (
+
+ )}
- {session.summary || 'New Session'}
+ {sessionName}
- {formatTimeAgo(session.lastActivity, currentTime)}
+ {formatTimeAgo(sessionTime, currentTime)}
- {session.messageCount > 0 && (
+ {messageCount > 0 && (
- {session.messageCount}
+ {messageCount}
)}
+ {/* Provider tiny icon */}
+
+ {isCursorSession ? (
+
+ ) : (
+
+ )}
+
- {/* Desktop hover buttons */}
+ {/* Desktop hover buttons - only for Claude sessions */}
+ {!isCursorSession && (
{editingSession === session.id ? (
<>
@@ -1168,6 +1204,7 @@ function Sidebar({
>
)}
+ )}
);
diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx
index 0c8eb0a..b0939c3 100644
--- a/src/components/ToolsSettings.jsx
+++ b/src/components/ToolsSettings.jsx
@@ -41,7 +41,16 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
const [activeTab, setActiveTab] = useState('tools');
const [jsonValidationError, setJsonValidationError] = useState('');
- // Common tool patterns
+ const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor'
+
+ // Cursor-specific states
+ const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
+ const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]);
+ const [cursorSkipPermissions, setCursorSkipPermissions] = useState(false);
+ const [newCursorCommand, setNewCursorCommand] = useState('');
+ const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState('');
+ const [cursorMcpServers, setCursorMcpServers] = useState([]);
+ // Common tool patterns for Claude
const commonTools = [
'Bash(git log:*)',
'Bash(git diff:*)',
@@ -58,7 +67,45 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
'WebFetch',
'WebSearch'
];
+
+ // Common shell commands for Cursor
+ const commonCursorCommands = [
+ 'Shell(ls)',
+ 'Shell(mkdir)',
+ 'Shell(cd)',
+ 'Shell(cat)',
+ 'Shell(echo)',
+ 'Shell(git status)',
+ 'Shell(git diff)',
+ 'Shell(git log)',
+ 'Shell(npm install)',
+ 'Shell(npm run)',
+ 'Shell(python)',
+ 'Shell(node)'
+ ];
+ // Fetch Cursor MCP servers
+ const fetchCursorMcpServers = async () => {
+ try {
+ const token = localStorage.getItem('auth-token');
+ const response = await fetch('/api/cursor/mcp', {
+ headers: {
+ 'Authorization': `Bearer ${token}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setCursorMcpServers(data.servers || []);
+ } else {
+ console.error('Failed to fetch Cursor MCP servers');
+ }
+ } catch (error) {
+ console.error('Error fetching Cursor MCP servers:', error);
+ }
+ };
+
// MCP API functions
const fetchMcpServers = async () => {
try {
@@ -268,7 +315,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
const loadSettings = async () => {
try {
- // Load from localStorage
+ // Load Claude settings from localStorage
const savedSettings = localStorage.getItem('claude-tools-settings');
if (savedSettings) {
@@ -284,9 +331,27 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
setSkipPermissions(false);
setProjectSortOrder('name');
}
+
+ // Load Cursor settings from localStorage
+ const savedCursorSettings = localStorage.getItem('cursor-tools-settings');
+
+ if (savedCursorSettings) {
+ const cursorSettings = JSON.parse(savedCursorSettings);
+ setCursorAllowedCommands(cursorSettings.allowedCommands || []);
+ setCursorDisallowedCommands(cursorSettings.disallowedCommands || []);
+ setCursorSkipPermissions(cursorSettings.skipPermissions || false);
+ } else {
+ // Set Cursor defaults
+ setCursorAllowedCommands([]);
+ setCursorDisallowedCommands([]);
+ setCursorSkipPermissions(false);
+ }
// Load MCP servers from API
await fetchMcpServers();
+
+ // Load Cursor MCP servers
+ await fetchCursorMcpServers();
} catch (error) {
console.error('Error loading tool settings:', error);
// Set defaults on error
@@ -302,7 +367,8 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
setSaveStatus(null);
try {
- const settings = {
+ // Save Claude settings
+ const claudeSettings = {
allowedTools,
disallowedTools,
skipPermissions,
@@ -310,9 +376,17 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
lastUpdated: new Date().toISOString()
};
+ // Save Cursor settings
+ const cursorSettings = {
+ allowedCommands: cursorAllowedCommands,
+ disallowedCommands: cursorDisallowedCommands,
+ skipPermissions: cursorSkipPermissions,
+ lastUpdated: new Date().toISOString()
+ };
// Save to localStorage
- localStorage.setItem('claude-tools-settings', JSON.stringify(settings));
+ localStorage.setItem('claude-tools-settings', JSON.stringify(claudeSettings));
+ localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings));
setSaveStatus('success');
@@ -635,6 +709,36 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
{activeTab === 'tools' && (
+ {/* Provider Tabs */}
+
+
+ setToolsProvider('claude')}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
+ toolsProvider === 'claude'
+ ? 'border-blue-600 text-blue-600 dark:text-blue-400'
+ : 'border-transparent text-muted-foreground hover:text-foreground'
+ }`}
+ >
+ Claude Tools
+
+ setToolsProvider('cursor')}
+ className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
+ toolsProvider === 'cursor'
+ ? 'border-purple-600 text-purple-600 dark:text-purple-400'
+ : 'border-transparent text-muted-foreground hover:text-foreground'
+ }`}
+ >
+ Cursor Tools
+
+
+
+
+ {/* Claude Tools Content */}
+ {toolsProvider === 'claude' && (
+
+
{/* Skip Permissions */}
@@ -1360,6 +1464,216 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) {
)}
)}
+
+ {/* Cursor Tools Content */}
+ {toolsProvider === 'cursor' && (
+
+
+ {/* Skip Permissions for Cursor */}
+
+
+
+
+ Cursor Permission Settings
+
+
+
+
+ setCursorSkipPermissions(e.target.checked)}
+ className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
+ />
+
+
+ Skip permission prompts (use with caution)
+
+
+ Equivalent to -f flag in Cursor CLI
+
+
+
+
+
+
+ {/* Allowed Shell Commands */}
+
+
+
+
+ Allowed Shell Commands
+
+
+
+ Shell commands that are automatically allowed without prompting for permission
+
+
+
+
setNewCursorCommand(e.target.value)}
+ placeholder='e.g., "Shell(ls)" or "Shell(git status)"'
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ if (newCursorCommand && !cursorAllowedCommands.includes(newCursorCommand)) {
+ setCursorAllowedCommands([...cursorAllowedCommands, newCursorCommand]);
+ setNewCursorCommand('');
+ }
+ }
+ }}
+ className="flex-1 h-10 touch-manipulation"
+ style={{ fontSize: '16px' }}
+ />
+
{
+ if (newCursorCommand && !cursorAllowedCommands.includes(newCursorCommand)) {
+ setCursorAllowedCommands([...cursorAllowedCommands, newCursorCommand]);
+ setNewCursorCommand('');
+ }
+ }}
+ disabled={!newCursorCommand}
+ size="sm"
+ className="h-10 px-4 touch-manipulation"
+ >
+
+ Add Command
+
+
+
+ {/* Common commands quick add */}
+
+
+ Quick add common commands:
+
+
+ {commonCursorCommands.map(cmd => (
+ {
+ if (!cursorAllowedCommands.includes(cmd)) {
+ setCursorAllowedCommands([...cursorAllowedCommands, cmd]);
+ }
+ }}
+ disabled={cursorAllowedCommands.includes(cmd)}
+ className="text-xs h-8 touch-manipulation truncate"
+ >
+ {cmd}
+
+ ))}
+
+
+
+
+ {cursorAllowedCommands.map(cmd => (
+
+
+ {cmd}
+
+ setCursorAllowedCommands(cursorAllowedCommands.filter(c => c !== cmd))}
+ className="text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
+ >
+
+
+
+ ))}
+ {cursorAllowedCommands.length === 0 && (
+
+ No allowed shell commands configured
+
+ )}
+
+
+
+ {/* Disallowed Shell Commands */}
+
+
+
+
+ Disallowed Shell Commands
+
+
+
+ Shell commands that should always be denied
+
+
+
+
setNewCursorDisallowedCommand(e.target.value)}
+ placeholder='e.g., "Shell(rm -rf)" or "Shell(sudo)"'
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ if (newCursorDisallowedCommand && !cursorDisallowedCommands.includes(newCursorDisallowedCommand)) {
+ setCursorDisallowedCommands([...cursorDisallowedCommands, newCursorDisallowedCommand]);
+ setNewCursorDisallowedCommand('');
+ }
+ }
+ }}
+ className="flex-1 h-10 touch-manipulation"
+ style={{ fontSize: '16px' }}
+ />
+
{
+ if (newCursorDisallowedCommand && !cursorDisallowedCommands.includes(newCursorDisallowedCommand)) {
+ setCursorDisallowedCommands([...cursorDisallowedCommands, newCursorDisallowedCommand]);
+ setNewCursorDisallowedCommand('');
+ }
+ }}
+ disabled={!newCursorDisallowedCommand}
+ size="sm"
+ className="h-10 px-4 touch-manipulation"
+ >
+
+ Add Command
+
+
+
+
+ {cursorDisallowedCommands.map(cmd => (
+
+
+ {cmd}
+
+ setCursorDisallowedCommands(cursorDisallowedCommands.filter(c => c !== cmd))}
+ className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
+ >
+
+
+
+ ))}
+ {cursorDisallowedCommands.length === 0 && (
+
+ No disallowed shell commands configured
+
+ )}
+
+
+
+ {/* Help Section */}
+
+
+ Cursor Shell Command Examples:
+
+
+ "Shell(ls)" - Allow ls command
+ "Shell(git status)" - Allow git status command
+ "Shell(mkdir)" - Allow mkdir command
+ "-f" flag - Skip all permission prompts (dangerous)
+
+
+
+ )}
+
+ )}
diff --git a/src/utils/api.js b/src/utils/api.js
index 49ae915..2297851 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -43,8 +43,16 @@ export const api = {
projects: () => authenticatedFetch('/api/projects'),
sessions: (projectName, limit = 5, offset = 0) =>
authenticatedFetch(`/api/projects/${projectName}/sessions?limit=${limit}&offset=${offset}`),
- sessionMessages: (projectName, sessionId) =>
- authenticatedFetch(`/api/projects/${projectName}/sessions/${sessionId}/messages`),
+ sessionMessages: (projectName, sessionId, limit = null, offset = 0) => {
+ const params = new URLSearchParams();
+ if (limit !== null) {
+ params.append('limit', limit);
+ params.append('offset', offset);
+ }
+ const queryString = params.toString();
+ const url = `/api/projects/${projectName}/sessions/${sessionId}/messages${queryString ? `?${queryString}` : ''}`;
+ return authenticatedFetch(url);
+ },
renameProject: (projectName, displayName) =>
authenticatedFetch(`/api/projects/${projectName}/rename`, {
method: 'PUT',
diff --git a/store.db-shm b/store.db-shm
new file mode 100644
index 0000000..fe9ac28
Binary files /dev/null and b/store.db-shm differ
diff --git a/store.db-wal b/store.db-wal
new file mode 100644
index 0000000..e69de29
diff --git a/test.html b/test.html
new file mode 100644
index 0000000..bdb58d2
--- /dev/null
+++ b/test.html
@@ -0,0 +1 @@
+hello world 5