mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-11 23:59:47 +00:00
feat: Implement Cursor session fetching and enhance message parsing in ChatInterface
This commit is contained in:
@@ -2,6 +2,10 @@ import { promises as fs } from 'fs';
|
|||||||
import fsSync from 'fs';
|
import fsSync from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import readline from 'readline';
|
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
|
// Cache for extracted project directories
|
||||||
const projectDirectoryCache = new Map();
|
const projectDirectoryCache = new Map();
|
||||||
@@ -207,6 +211,14 @@ async function getProjects() {
|
|||||||
console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
|
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);
|
projects.push(project);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,9 +248,17 @@ async function getProjects() {
|
|||||||
fullPath: actualProjectDir,
|
fullPath: actualProjectDir,
|
||||||
isCustomName: !!projectConfig.displayName,
|
isCustomName: !!projectConfig.displayName,
|
||||||
isManuallyAdded: true,
|
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);
|
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 {
|
export {
|
||||||
getProjects,
|
getProjects,
|
||||||
|
|||||||
@@ -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 = [];
|
const messages = [];
|
||||||
for (const blob of blobs) {
|
for (const blob of blobs) {
|
||||||
try {
|
try {
|
||||||
// Attempt direct JSON parse first
|
// Attempt direct JSON parse first
|
||||||
const raw = blob.data.toString('utf8');
|
const raw = blob.data.toString('utf8');
|
||||||
let parsed;
|
let parsed;
|
||||||
|
let isValidJson = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(raw);
|
parsed = JSON.parse(raw);
|
||||||
|
isValidJson = true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// If not JSON, try to extract JSON from within binary-looking string
|
// If not JSON, try to extract JSON from within binary-looking string
|
||||||
const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
|
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);
|
const jsonStr = cleaned.slice(start, end + 1);
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(jsonStr);
|
parsed = JSON.parse(jsonStr);
|
||||||
|
isValidJson = true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
parsed = null;
|
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 });
|
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) {
|
} catch (e) {
|
||||||
messages.push({ id: blob.id, content: blob.data.toString() });
|
// Skip blobs that cause errors
|
||||||
|
console.log(`Skipping blob ${blob.id}: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ClaudeStatus from './ClaudeStatus';
|
|||||||
import { MicButton } from './MicButton.jsx';
|
import { MicButton } from './MicButton.jsx';
|
||||||
import { api, authenticatedFetch } from '../utils/api';
|
import { api, authenticatedFetch } from '../utils/api';
|
||||||
|
|
||||||
|
|
||||||
// Safe localStorage utility to handle quota exceeded errors
|
// Safe localStorage utility to handle quota exceeded errors
|
||||||
const safeLocalStorage = {
|
const safeLocalStorage = {
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
@@ -180,7 +181,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Claude/Error messages on the left */
|
/* Claude/Error/Tool messages on the left */
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{!isGrouped && (
|
{!isGrouped && (
|
||||||
<div className="flex items-center space-x-3 mb-2">
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
@@ -188,6 +189,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
<div className="w-8 h-8 bg-red-600 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0">
|
<div className="w-8 h-8 bg-red-600 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0">
|
||||||
!
|
!
|
||||||
</div>
|
</div>
|
||||||
|
) : message.type === 'tool' ? (
|
||||||
|
<div className="w-8 h-8 bg-gray-600 dark:bg-gray-700 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0">
|
||||||
|
🔧
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1">
|
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white text-sm flex-shrink-0 p-1">
|
||||||
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? (
|
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? (
|
||||||
@@ -198,7 +203,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{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')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -330,11 +335,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
})()}
|
})()}
|
||||||
{message.toolInput && message.toolName !== 'Edit' && (() => {
|
{message.toolInput && message.toolName !== 'Edit' && (() => {
|
||||||
// Debug log to see what we're dealing with
|
// 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
|
// Special handling for Write tool
|
||||||
if (message.toolName === 'Write') {
|
if (message.toolName === 'Write') {
|
||||||
console.log('Write tool detected, toolInput:', message.toolInput);
|
|
||||||
try {
|
try {
|
||||||
let input;
|
let input;
|
||||||
// Handle both JSON string and already parsed object
|
// Handle both JSON string and already parsed object
|
||||||
@@ -344,7 +347,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
input = message.toolInput;
|
input = message.toolInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Parsed Write input:', input);
|
|
||||||
|
|
||||||
if (input.file_path && input.content !== undefined) {
|
if (input.file_path && input.content !== undefined) {
|
||||||
return (
|
return (
|
||||||
@@ -1003,6 +1005,20 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{/* Thinking accordion for reasoning */}
|
||||||
|
{message.reasoning && (
|
||||||
|
<details className="mb-3">
|
||||||
|
<summary className="cursor-pointer text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 font-medium">
|
||||||
|
💭 Thinking...
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 pl-4 border-l-2 border-gray-300 dark:border-gray-600 italic text-gray-600 dark:text-gray-400 text-sm">
|
||||||
|
<div className="whitespace-pre-wrap">
|
||||||
|
{message.reasoning}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
|
||||||
{message.type === 'assistant' ? (
|
{message.type === 'assistant' ? (
|
||||||
<div className="prose prose-sm max-w-none dark:prose-invert prose-gray [&_code]:!bg-transparent [&_code]:!p-0">
|
<div className="prose prose-sm max-w-none dark:prose-invert prose-gray [&_code]:!bg-transparent [&_code]:!p-0">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@@ -1271,49 +1287,239 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const blobs = data?.session?.messages || [];
|
const blobs = data?.session?.messages || [];
|
||||||
const converted = [];
|
const converted = [];
|
||||||
const now = Date.now();
|
const toolUseMap = {}; // Map to store tool uses by ID for linking results
|
||||||
let idx = 0;
|
|
||||||
for (const blob of blobs) {
|
// First pass: process all messages maintaining order
|
||||||
|
for (let blobIdx = 0; blobIdx < blobs.length; blobIdx++) {
|
||||||
|
const blob = blobs[blobIdx];
|
||||||
const content = blob.content;
|
const content = blob.content;
|
||||||
let text = '';
|
let text = '';
|
||||||
let role = 'assistant';
|
let role = 'assistant';
|
||||||
|
let reasoningText = null; // Move to outer scope
|
||||||
try {
|
try {
|
||||||
if (typeof content === 'string') {
|
// Handle different Cursor message formats
|
||||||
// Attempt to extract embedded JSON first
|
if (content?.role && content?.content) {
|
||||||
const cleaned = content.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
|
// Direct format: {"role":"user","content":[{"type":"text","text":"..."}]}
|
||||||
let extractedTexts = [];
|
// Skip system messages
|
||||||
const start = cleaned.indexOf('{');
|
if (content.role === 'system') {
|
||||||
const end = cleaned.lastIndexOf('}');
|
|
||||||
if (start !== -1 && end !== -1 && end > 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// JSON parse failed; fall back to cleaned text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (extractedTexts.length > 0) {
|
|
||||||
extractedTexts.forEach(t => converted.push({ type: 'assistant', content: t, timestamp: new Date(now + (idx++)) }));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// No JSON; use cleaned readable text if any
|
|
||||||
const readable = cleaned.trim();
|
// Handle tool messages
|
||||||
if (readable) {
|
if (content.role === 'tool') {
|
||||||
// Heuristic: short single token like 'hey' → user, otherwise assistant
|
// Tool result format - find the matching tool use message and update it
|
||||||
const isLikelyUser = /^[a-zA-Z0-9.,!?\s]{1,10}$/.test(readable) && readable.toLowerCase().includes('hey');
|
if (Array.isArray(content.content)) {
|
||||||
role = isLikelyUser ? 'user' : 'assistant';
|
for (const item of content.content) {
|
||||||
text = readable;
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
text = '';
|
text = '';
|
||||||
}
|
}
|
||||||
|
} else if (typeof content.content === 'string') {
|
||||||
|
text = content.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (content?.message?.role && content?.message?.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';
|
role = content.message.role === 'user' ? 'user' : 'assistant';
|
||||||
if (Array.isArray(content.message.content)) {
|
if (Array.isArray(content.message.content)) {
|
||||||
text = content.message.content
|
text = content.message.content
|
||||||
@@ -1322,26 +1528,38 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
} else if (typeof content.message.content === 'string') {
|
} else if (typeof content.message.content === 'string') {
|
||||||
text = content.message.content;
|
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) {
|
} catch (e) {
|
||||||
text = String(content);
|
console.log('Error parsing blob content:', e);
|
||||||
}
|
}
|
||||||
if (text && text.trim()) {
|
if (text && text.trim()) {
|
||||||
converted.push({
|
const message = {
|
||||||
type: role,
|
type: role,
|
||||||
content: text,
|
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;
|
return converted;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error loading Cursor session messages:', 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
|
// For other cursor-system messages, avoid dumping raw objects to chat
|
||||||
console.log('Cursor system message:', latestMessage.data);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Error handling cursor-system message:', e);
|
console.warn('Error handling cursor-system message:', e);
|
||||||
}
|
}
|
||||||
@@ -1873,7 +2090,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
case 'cursor-user':
|
case 'cursor-user':
|
||||||
// Handle Cursor user messages (usually echoes)
|
// 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
|
// Don't add user messages as they're already shown from input
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1994,7 +2210,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
case 'claude-status':
|
case 'claude-status':
|
||||||
// Handle Claude working status messages
|
// Handle Claude working status messages
|
||||||
console.log('🔔 Received claude-status message:', latestMessage);
|
|
||||||
const statusData = latestMessage.data;
|
const statusData = latestMessage.data;
|
||||||
if (statusData) {
|
if (statusData) {
|
||||||
// Parse the status message to extract relevant information
|
// 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;
|
statusInfo.can_interrupt = statusData.can_interrupt;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📊 Setting claude status:', statusInfo);
|
|
||||||
setClaudeStatus(statusInfo);
|
setClaudeStatus(statusInfo);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setCanAbortSession(statusInfo.can_interrupt);
|
setCanAbortSession(statusInfo.can_interrupt);
|
||||||
@@ -2622,12 +2836,118 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
</div>
|
</div>
|
||||||
) : chatMessages.length === 0 ? (
|
) : chatMessages.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
|
{!selectedSession && (
|
||||||
|
<div className="text-center px-6 sm:px-4 py-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">Choose Your AI Assistant</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Select a provider to start a new conversation
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8">
|
||||||
|
{/* Claude Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||||
|
<ClaudeLogo className="w-10 h-10" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-white">Claude</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">by Anthropic</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{provider === 'claude' && (
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<div className="w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Cursor Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-3">
|
||||||
|
<CursorLogo className="w-10 h-10" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-white">Cursor</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">AI Code Editor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{provider === 'cursor' && (
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<div className="w-5 h-5 bg-purple-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Model Selection for Cursor - Always reserve space to prevent jumping */}
|
||||||
|
<div className={`mb-6 transition-opacity duration-200 ${provider === 'cursor' ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
{provider === 'cursor' ? 'Select Model' : '\u00A0'}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={cursorModel}
|
||||||
|
onChange={(e) => {
|
||||||
|
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'}
|
||||||
|
>
|
||||||
|
<option value="gpt-5">GPT-5</option>
|
||||||
|
<option value="sonnet-4">Sonnet-4</option>
|
||||||
|
<option value="opus-4.1">Opus 4.1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{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'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedSession && (
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400 px-6 sm:px-4">
|
<div className="text-center text-gray-500 dark:text-gray-400 px-6 sm:px-4">
|
||||||
<p className="font-bold text-lg sm:text-xl mb-3">Start a conversation with Claude</p>
|
<p className="font-bold text-lg sm:text-xl mb-3">Continue your conversation</p>
|
||||||
<p className="text-sm sm:text-base leading-relaxed">
|
<p className="text-sm sm:text-base leading-relaxed">
|
||||||
Ask questions about your code, request changes, or get help with development tasks
|
Ask questions about your code, request changes, or get help with development tasks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -2734,6 +3054,56 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
)}
|
)}
|
||||||
<span className="text-sm capitalize">{selectedSession.__provider}</span>
|
<span className="text-sm capitalize">{selectedSession.__provider}</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : chatMessages.length === 0 ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<ClaudeLogo className="w-4 h-4" />
|
||||||
|
<span>Claude</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<CursorLogo className="w-4 h-4" />
|
||||||
|
<span>Cursor</span>
|
||||||
|
</button>
|
||||||
|
{/* Always reserve space for model dropdown to prevent jumping */}
|
||||||
|
<div className={`transition-opacity duration-200 ${provider === 'cursor' ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
|
||||||
|
<select
|
||||||
|
value={cursorModel}
|
||||||
|
onChange={(e) => {
|
||||||
|
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'}
|
||||||
|
>
|
||||||
|
<option value="gpt-5">GPT-5</option>
|
||||||
|
<option value="sonnet-4">Sonnet-4</option>
|
||||||
|
<option value="opus-4.1">Opus 4.1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<select
|
<select
|
||||||
@@ -2749,7 +3119,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
<option value="claude">Claude</option>
|
<option value="claude">Claude</option>
|
||||||
<option value="cursor">Cursor</option>
|
<option value="cursor">Cursor</option>
|
||||||
</select>
|
</select>
|
||||||
{provider === 'cursor' && (
|
{/* Always reserve space for model dropdown to prevent jumping */}
|
||||||
|
<div className={`transition-opacity duration-200 ${provider === 'cursor' ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
|
||||||
<select
|
<select
|
||||||
value={cursorModel}
|
value={cursorModel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -2757,14 +3128,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
setCursorModel(newModel);
|
setCursorModel(newModel);
|
||||||
localStorage.setItem('cursor-model', 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"
|
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}
|
disabled={isLoading || provider !== 'cursor'}
|
||||||
>
|
>
|
||||||
<option value="gpt-5">GPT-5</option>
|
<option value="gpt-5">GPT-5</option>
|
||||||
<option value="sonnet-4">Sonnet-4</option>
|
<option value="sonnet-4">Sonnet-4</option>
|
||||||
<option value="opus-4.1">Opus 4.1</option>
|
<option value="opus-4.1">Opus 4.1</option>
|
||||||
</select>
|
</select>
|
||||||
)}
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,14 +60,12 @@ function GitPanel({ selectedProject, isMobile }) {
|
|||||||
const fetchGitStatus = async () => {
|
const fetchGitStatus = async () => {
|
||||||
if (!selectedProject) return;
|
if (!selectedProject) return;
|
||||||
|
|
||||||
console.log('Fetching git status for project:', selectedProject.name, 'path:', selectedProject.path);
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
console.log('Git status response:', data);
|
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
console.error('Git status error:', data.error);
|
console.error('Git status error:', data.error);
|
||||||
|
|||||||
Reference in New Issue
Block a user