refactor: improve shell performance, fix bugs on the git tab and promote login to a standalone component

Implement PTY session persistence with 30-minute timeout for shell reconnection. Sessions are now keyed by project path and session ID, preserving terminal state across UI disconnections with buffered output replay.
Refactor Shell component to use refs for stable prop access, removing unnecessary isActive prop and improving WebSocket connection lifecycle management. Replace conditional rendering with early returns in MainContent for better performance.
Add directory handling in git operations: support discarding, diffing, and viewing directories in untracked files. Prevent errors when staging or generating commit messages for directories.
Extract LoginModal into reusable component for Claude and Cursor CLI authentication. Add minimal mode to StandaloneShell for embedded use cases. Update Settings to use new LoginModal component.
Improve terminal dimensions handling by passing client-provided cols and rows to PTY spawn. Add comprehensive logging for session lifecycle and API operations.
This commit is contained in:
simos
2025-11-14 23:44:29 +00:00
parent 2815e206dc
commit 521fce32d0
8 changed files with 467 additions and 426 deletions

View File

@@ -161,10 +161,18 @@ router.get('/diff', async (req, res) => {
let diff;
if (isUntracked) {
// For untracked files, show the entire file content as additions
const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
const lines = fileContent.split('\n');
diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
lines.map(line => `+${line}`).join('\n');
const filePath = path.join(projectPath, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
// For directories, show a simple message
diff = `Directory: ${file}\n(Cannot show diff for directories)`;
} else {
const fileContent = await fs.readFile(filePath, 'utf-8');
const lines = fileContent.split('\n');
diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` +
lines.map(line => `+${line}`).join('\n');
}
} else if (isDeleted) {
// For deleted files, show the entire file content from HEAD as deletions
const { stdout: fileContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath });
@@ -222,7 +230,15 @@ router.get('/file-with-diff', async (req, res) => {
currentContent = headContent; // Show the deleted content in editor
} else {
// Get current file content
currentContent = await fs.readFile(path.join(projectPath, file), 'utf-8');
const filePath = path.join(projectPath, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
// Cannot show content for directories
return res.status(400).json({ error: 'Cannot show diff for directories' });
}
currentContent = await fs.readFile(filePath, 'utf-8');
if (!isUntracked) {
// Get the old content from HEAD for tracked files
@@ -474,8 +490,14 @@ router.post('/generate-commit-message', async (req, res) => {
for (const file of files) {
try {
const filePath = path.join(projectPath, file);
const content = await fs.readFile(filePath, 'utf-8');
diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
const stats = await fs.stat(filePath);
if (!stats.isDirectory()) {
const content = await fs.readFile(filePath, 'utf-8');
diffContext += `\n--- ${file} (new file) ---\n${content.substring(0, 1000)}\n`;
} else {
diffContext += `\n--- ${file} (new directory) ---\n`;
}
} catch (error) {
console.error(`Error reading file ${file}:`, error);
}
@@ -976,10 +998,17 @@ router.post('/discard', async (req, res) => {
}
const status = statusOutput.substring(0, 2);
if (status === '??') {
// Untracked file - delete it
await fs.unlink(path.join(projectPath, file));
// Untracked file or directory - delete it
const filePath = path.join(projectPath, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await fs.rm(filePath, { recursive: true, force: true });
} else {
await fs.unlink(filePath);
}
} else if (status.includes('M') || status.includes('D')) {
// Modified or deleted file - restore from HEAD
await execAsync(`git restore "${file}"`, { cwd: projectPath });
@@ -1020,10 +1049,18 @@ router.post('/delete-untracked', async (req, res) => {
return res.status(400).json({ error: 'File is not untracked. Use discard for tracked files.' });
}
// Delete the untracked file
await fs.unlink(path.join(projectPath, file));
res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
// Delete the untracked file or directory
const filePath = path.join(projectPath, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
// Use rm with recursive option for directories
await fs.rm(filePath, { recursive: true, force: true });
res.json({ success: true, message: `Untracked directory ${file} deleted successfully` });
} else {
await fs.unlink(filePath);
res.json({ success: true, message: `Untracked file ${file} deleted successfully` });
}
} catch (error) {
console.error('Git delete untracked error:', error);
res.status(500).json({ error: error.message });