feat: Multi-session support with tabs, split view, and Mochi integration
- Add SessionContext for central state management - Add TabBar component for session tabs - Add SplitLayout for side-by-side session viewing - Add ChatPanel wrapper component - Refactor ChatInput to uncontrolled input for performance - Add SCP file transfer for SSH hosts (Mochi) - Fix stats undefined crash on session restore - Store host info in sessions for upload routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -304,28 +304,60 @@ app.get('/api/browse', async (req, res) => {
|
||||
});
|
||||
|
||||
// File upload endpoint
|
||||
app.post('/api/upload/:sessionId', upload.array('files', 5), (req, res) => {
|
||||
app.post('/api/upload/:sessionId', upload.array('files', 5), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({ error: 'No files uploaded' });
|
||||
}
|
||||
|
||||
const uploadedFiles = req.files.map(file => {
|
||||
const sessionId = req.params.sessionId;
|
||||
const session = sessions.get(sessionId);
|
||||
const isSSH = session?.host?.connection?.type === 'ssh';
|
||||
|
||||
const uploadedFiles = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
// Convert container path to host path for Claude
|
||||
// /projects/.claude-uploads/... -> /home/sumdex/projects/.claude-uploads/...
|
||||
const hostPath = file.path.replace('/projects/', '/home/sumdex/projects/');
|
||||
return {
|
||||
let hostPath = file.path.replace('/projects/', '/home/sumdex/projects/');
|
||||
|
||||
// For SSH hosts, transfer file via SCP
|
||||
if (isSSH && session.host) {
|
||||
const { host: sshHost, user, port = 22 } = session.host.connection;
|
||||
const remotePath = `/tmp/.claude-uploads/${file.filename}`;
|
||||
|
||||
try {
|
||||
// Create remote directory if needed
|
||||
const { execSync } = await import('child_process');
|
||||
execSync(`ssh -o StrictHostKeyChecking=no -p ${port} ${user}@${sshHost} "mkdir -p /tmp/.claude-uploads"`, {
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
// Transfer file via SCP
|
||||
execSync(`scp -o StrictHostKeyChecking=no -P ${port} "${file.path}" ${user}@${sshHost}:"${remotePath}"`, {
|
||||
timeout: 60000 // 60s for large files
|
||||
});
|
||||
|
||||
hostPath = remotePath;
|
||||
console.log(`[Upload] SCP transferred ${file.filename} to ${session.hostId}:${remotePath}`);
|
||||
} catch (scpErr) {
|
||||
console.error(`[Upload] SCP error for ${file.filename}:`, scpErr.message);
|
||||
// Fall back to local path (won't work but at least doesn't fail)
|
||||
}
|
||||
}
|
||||
|
||||
uploadedFiles.push({
|
||||
originalName: file.originalname,
|
||||
savedName: file.filename,
|
||||
path: hostPath, // Use host path so Claude can read it
|
||||
containerPath: file.path, // Keep container path for reference
|
||||
path: hostPath,
|
||||
containerPath: file.path,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
isImage: file.mimetype.startsWith('image/')
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[Upload] Session ${req.params.sessionId}: ${uploadedFiles.length} files uploaded`);
|
||||
console.log(`[Upload] Session ${sessionId}: ${uploadedFiles.length} files uploaded`);
|
||||
res.json({ files: uploadedFiles });
|
||||
} catch (err) {
|
||||
console.error('[Upload] Error:', err);
|
||||
@@ -582,8 +614,11 @@ wss.on('connection', (ws, req) => {
|
||||
const { host: sshHost, user, port = 22 } = host.connection;
|
||||
const sshTarget = `${user}@${sshHost}`;
|
||||
|
||||
// Use claudePath from config if specified, otherwise default to 'claude'
|
||||
const claudeBin = host.claudePath || 'claude';
|
||||
|
||||
// Build the remote command with PATH setup for non-login shells
|
||||
const remoteCmd = `export PATH="$HOME/.local/bin:$PATH" && cd ${projectPath} && claude ${claudeArgs.join(' ')}`;
|
||||
const remoteCmd = `export PATH="$HOME/.local/bin:$PATH" && cd ${projectPath} && ${claudeBin} ${claudeArgs.join(' ')}`;
|
||||
|
||||
console.log(`[${sessionId}] SSH to ${sshTarget}:${port} - ${remoteCmd}`);
|
||||
|
||||
@@ -607,7 +642,7 @@ wss.on('connection', (ws, req) => {
|
||||
});
|
||||
}
|
||||
|
||||
sessions.set(sessionId, { process: claudeProcess, project: projectPath });
|
||||
sessions.set(sessionId, { process: claudeProcess, project: projectPath, host: host, hostId: hostId });
|
||||
|
||||
sendToClient('session_started', {
|
||||
sessionId,
|
||||
|
||||
Reference in New Issue
Block a user