diff --git a/server/projects.js b/server/projects.js
index b7189d3..1f22895 100755
--- a/server/projects.js
+++ b/server/projects.js
@@ -2,6 +2,10 @@ import { promises as fs } from 'fs';
import fsSync from 'fs';
import path from 'path';
import readline from 'readline';
+import crypto from 'crypto';
+import sqlite3 from 'sqlite3';
+import { open } from 'sqlite';
+import os from 'os';
// Cache for extracted project directories
const projectDirectoryCache = new Map();
@@ -207,6 +211,14 @@ async function getProjects() {
console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
}
+ // Also fetch Cursor sessions for this project
+ try {
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
+ } catch (e) {
+ console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
+ project.cursorSessions = [];
+ }
+
projects.push(project);
}
}
@@ -236,9 +248,17 @@ async function getProjects() {
fullPath: actualProjectDir,
isCustomName: !!projectConfig.displayName,
isManuallyAdded: true,
- sessions: []
+ sessions: [],
+ cursorSessions: []
};
+ // Try to fetch Cursor sessions for manual projects too
+ try {
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
+ } catch (e) {
+ console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
+ }
+
projects.push(project);
}
}
@@ -615,6 +635,117 @@ async function addProjectManually(projectPath, displayName = null) {
};
}
+// Fetch Cursor sessions for a given project path
+async function getCursorSessions(projectPath) {
+ try {
+ // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
+ const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
+ const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
+
+ // Check if the directory exists
+ try {
+ await fs.access(cursorChatsPath);
+ } catch (error) {
+ // No sessions for this project
+ return [];
+ }
+
+ // List all session directories
+ const sessionDirs = await fs.readdir(cursorChatsPath);
+ const sessions = [];
+
+ for (const sessionId of sessionDirs) {
+ const sessionPath = path.join(cursorChatsPath, sessionId);
+ const storeDbPath = path.join(sessionPath, 'store.db');
+
+ try {
+ // Check if store.db exists
+ await fs.access(storeDbPath);
+
+ // Capture store.db mtime as a reliable fallback timestamp
+ let dbStatMtimeMs = null;
+ try {
+ const stat = await fs.stat(storeDbPath);
+ dbStatMtimeMs = stat.mtimeMs;
+ } catch (_) {}
+
+ // Open SQLite database
+ const db = await open({
+ filename: storeDbPath,
+ driver: sqlite3.Database,
+ mode: sqlite3.OPEN_READONLY
+ });
+
+ // Get metadata from meta table
+ const metaRows = await db.all(`
+ SELECT key, value FROM meta
+ `);
+
+ // Parse metadata
+ let metadata = {};
+ for (const row of metaRows) {
+ if (row.value) {
+ try {
+ // Try to decode as hex-encoded JSON
+ const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
+ if (hexMatch) {
+ const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
+ metadata[row.key] = JSON.parse(jsonStr);
+ } else {
+ metadata[row.key] = row.value.toString();
+ }
+ } catch (e) {
+ metadata[row.key] = row.value.toString();
+ }
+ }
+ }
+
+ // Get message count
+ const messageCountResult = await db.get(`
+ SELECT COUNT(*) as count FROM blobs
+ `);
+
+ await db.close();
+
+ // Extract session info
+ const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
+
+ // Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
+ let createdAt = null;
+ if (metadata.createdAt) {
+ createdAt = new Date(metadata.createdAt).toISOString();
+ } else if (dbStatMtimeMs) {
+ createdAt = new Date(dbStatMtimeMs).toISOString();
+ } else {
+ createdAt = new Date().toISOString();
+ }
+
+ sessions.push({
+ id: sessionId,
+ name: sessionName,
+ createdAt: createdAt,
+ lastActivity: createdAt, // For compatibility with Claude sessions
+ messageCount: messageCountResult.count || 0,
+ projectPath: projectPath
+ });
+
+ } catch (error) {
+ console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
+ }
+ }
+
+ // Sort sessions by creation time (newest first)
+ sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+
+ // Return only the first 5 sessions for performance
+ return sessions.slice(0, 5);
+
+ } catch (error) {
+ console.error('Error fetching Cursor sessions:', error);
+ return [];
+ }
+}
+
export {
getProjects,
diff --git a/server/routes/cursor.js b/server/routes/cursor.js
index 61d201e..5ff5da5 100644
--- a/server/routes/cursor.js
+++ b/server/routes/cursor.js
@@ -622,15 +622,18 @@ router.get('/sessions/:sessionId', async (req, res) => {
}
}
- // Parse blob data to extract messages
+ // Parse blob data to extract messages - only include blobs with valid JSON
const messages = [];
for (const blob of blobs) {
try {
// Attempt direct JSON parse first
const raw = blob.data.toString('utf8');
let parsed;
+ let isValidJson = false;
+
try {
parsed = JSON.parse(raw);
+ isValidJson = true;
} catch (_) {
// If not JSON, try to extract JSON from within binary-looking string
const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
@@ -640,20 +643,28 @@ router.get('/sessions/:sessionId', async (req, res) => {
const jsonStr = cleaned.slice(start, end + 1);
try {
parsed = JSON.parse(jsonStr);
+ isValidJson = true;
} catch (_) {
parsed = null;
+ isValidJson = false;
}
}
}
- if (parsed) {
+
+ // Only include blobs that contain valid JSON data
+ if (isValidJson && parsed) {
+ // Filter out ONLY system messages at the server level
+ // Check both direct role and nested message.role
+ const role = parsed?.role || parsed?.message?.role;
+ if (role === 'system') {
+ continue; // Skip only system messages
+ }
messages.push({ id: blob.id, content: parsed });
- } else {
- // Fallback to cleaned text content
- const text = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '').trim();
- messages.push({ id: blob.id, content: text });
}
+ // Skip non-JSON blobs (binary data) completely
} catch (e) {
- messages.push({ id: blob.id, content: blob.data.toString() });
+ // Skip blobs that cause errors
+ console.log(`Skipping blob ${blob.id}: ${e.message}`);
}
}
diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx
index 85fcbc0..58f761d 100644
--- a/src/components/ChatInterface.jsx
+++ b/src/components/ChatInterface.jsx
@@ -27,6 +27,7 @@ import ClaudeStatus from './ClaudeStatus';
import { MicButton } from './MicButton.jsx';
import { api, authenticatedFetch } from '../utils/api';
+
// Safe localStorage utility to handle quota exceeded errors
const safeLocalStorage = {
setItem: (key, value) => {
@@ -180,7 +181,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)}
) : (
- /* Claude/Error messages on the left */
+ /* Claude/Error/Tool messages on the left */
{!isGrouped && (
@@ -188,6 +189,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
!
+ ) : message.type === 'tool' ? (
+
+ 🔧
+
) : (
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? (
@@ -198,7 +203,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)}
- {message.type === 'error' ? 'Error' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')}
+ {message.type === 'error' ? 'Error' : message.type === 'tool' ? 'Tool' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')}
)}
@@ -330,11 +335,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
})()}
{message.toolInput && message.toolName !== 'Edit' && (() => {
// Debug log to see what we're dealing with
- console.log('Tool display - name:', message.toolName, 'input type:', typeof message.toolInput);
// Special handling for Write tool
if (message.toolName === 'Write') {
- console.log('Write tool detected, toolInput:', message.toolInput);
try {
let input;
// Handle both JSON string and already parsed object
@@ -344,7 +347,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
input = message.toolInput;
}
- console.log('Parsed Write input:', input);
if (input.file_path && input.content !== undefined) {
return (
@@ -1003,6 +1005,20 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
) : (
+ {/* Thinking accordion for reasoning */}
+ {message.reasoning && (
+
+
+ 💠Thinking...
+
+
+
+ {message.reasoning}
+
+
+
+ )}
+
{message.type === 'assistant' ? (
start) {
- const jsonStr = cleaned.slice(start, end + 1);
- try {
- const parsed = JSON.parse(jsonStr);
- if (parsed && parsed.content && Array.isArray(parsed.content)) {
- for (const part of parsed.content) {
- if (part?.type === 'text' && part.text) {
- extractedTexts.push(part.text);
+ // 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),
+ blobId: blob.id,
+ isToolUse: true,
+ toolName: toolName,
+ toolId: toolCallId,
+ toolInput: null,
+ toolResult: {
+ content: result,
+ isError: false
+ }
+ });
}
}
}
- } catch (_) {
- // JSON parse failed; fall back to cleaned text
+ }
+ 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),
+ blobId: blob.id
+ });
+ 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),
+ blobId: blob.id,
+ 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),
+ blobId: blob.id
+ });
+ 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),
+ blobId: blob.id,
+ 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),
+ blobId: blob.id
+ });
+ text = ''; // Clear to avoid duplicate
+ }
+ } else {
+ text = '';
+ }
+ } else if (typeof content.content === 'string') {
+ text = content.content;
}
}
- if (extractedTexts.length > 0) {
- extractedTexts.forEach(t => converted.push({ type: 'assistant', content: t, timestamp: new Date(now + (idx++)) }));
+ } else if (content?.message?.role && content?.message?.content) {
+ // Nested message format
+ if (content.message.role === 'system') {
continue;
}
- // No JSON; use cleaned readable text if any
- const readable = cleaned.trim();
- if (readable) {
- // Heuristic: short single token like 'hey' → user, otherwise assistant
- const isLikelyUser = /^[a-zA-Z0-9.,!?\s]{1,10}$/.test(readable) && readable.toLowerCase().includes('hey');
- role = isLikelyUser ? 'user' : 'assistant';
- text = readable;
- } else {
- text = '';
- }
- } else if (content?.message?.role && content?.message?.content) {
role = content.message.role === 'user' ? 'user' : 'assistant';
if (Array.isArray(content.message.content)) {
text = content.message.content
@@ -1322,26 +1528,38 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
.join('\n');
} else if (typeof content.message.content === 'string') {
text = content.message.content;
- } else {
- text = JSON.stringify(content.message.content);
}
- } else if (content?.content) {
- // Some Cursor blobs may have { content: string }
- text = typeof content.content === 'string' ? content.content : JSON.stringify(content.content);
- } else {
- text = JSON.stringify(content);
}
} catch (e) {
- text = String(content);
+ console.log('Error parsing blob content:', e);
}
if (text && text.trim()) {
- converted.push({
+ const message = {
type: role,
content: text,
- timestamp: new Date(now + (idx++))
- });
+ timestamp: new Date(Date.now() + blobIdx),
+ blobId: blob.id
+ };
+
+ // Add reasoning if we have it
+ if (reasoningText) {
+ message.reasoning = reasoningText;
+ }
+
+ converted.push(message);
}
}
+
+ // Sort messages by blob ID to maintain chronological order
+ converted.sort((a, b) => {
+ // First sort by blobId if available
+ if (a.blobId && b.blobId) {
+ return parseInt(a.blobId) - parseInt(b.blobId);
+ }
+ // Fallback to timestamp
+ return new Date(a.timestamp) - new Date(b.timestamp);
+ });
+
return converted;
} catch (e) {
console.error('Error loading Cursor session messages:', e);
@@ -1865,7 +2083,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
}
}
// For other cursor-system messages, avoid dumping raw objects to chat
- console.log('Cursor system message:', latestMessage.data);
} catch (e) {
console.warn('Error handling cursor-system message:', e);
}
@@ -1873,7 +2090,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
case 'cursor-user':
// Handle Cursor user messages (usually echoes)
- console.log('Cursor user message:', latestMessage.data);
// Don't add user messages as they're already shown from input
break;
@@ -1994,7 +2210,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
@@ -2025,7 +2240,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);
@@ -2622,12 +2836,118 @@ 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 && (
+
+
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
+
+
+ )}
) : (
<>
@@ -2734,6 +3054,56 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
)}
{selectedSession.__provider}
+ ) : chatMessages.length === 0 ? (
+
+
{
+ setProvider('claude');
+ localStorage.setItem('selected-provider', 'claude');
+ }}
+ className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg transition-all ${
+ provider === 'claude'
+ ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-700'
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ disabled={isLoading}
+ >
+
+ Claude
+
+
{
+ setProvider('cursor');
+ localStorage.setItem('selected-provider', 'cursor');
+ }}
+ className={`flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg transition-all ${
+ provider === 'cursor'
+ ? 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border border-purple-300 dark:border-purple-700'
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-700'
+ }`}
+ disabled={isLoading}
+ >
+
+ Cursor
+
+ {/* Always reserve space for model dropdown to prevent jumping */}
+
+ {
+ const newModel = e.target.value;
+ setCursorModel(newModel);
+ localStorage.setItem('cursor-model', newModel);
+ }}
+ className="pl-3 pr-8 py-1.5 text-sm bg-gray-100 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-[120px]"
+ disabled={isLoading || provider !== 'cursor'}
+ >
+ GPT-5
+ Sonnet-4
+ Opus 4.1
+
+
+
) : (
<>
Claude
Cursor
- {provider === 'cursor' && (
+ {/* Always reserve space for model dropdown to prevent jumping */}
+
{
@@ -2757,14 +3128,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
setCursorModel(newModel);
localStorage.setItem('cursor-model', newModel);
}}
- className="px-3 py-1.5 text-sm bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
- disabled={isLoading}
+ className="pl-3 pr-8 py-1.5 text-sm bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-[120px]"
+ disabled={isLoading || provider !== 'cursor'}
>
GPT-5
Sonnet-4
Opus 4.1
- )}
+
>
)}
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/test.html b/test.html
new file mode 100644
index 0000000..7f005fd
--- /dev/null
+++ b/test.html
@@ -0,0 +1,2 @@
+world world 3
+world world 4