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:
2025-12-17 14:16:52 +01:00
parent 960f2e137d
commit cfee1711dc
9 changed files with 2122 additions and 467 deletions

View File

@@ -0,0 +1,926 @@
import { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo } from 'react';
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://100.105.142.13:3001';
const API_URL = import.meta.env.VITE_API_URL || 'http://100.105.142.13:3001';
const SESSIONS_STORAGE_KEY = 'claude-webui-sessions';
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
// Generate unique session ID
function generateSessionId() {
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Create initial session state (messages are stored separately for performance)
function createSessionState(id, host = 'neko', project = '/home/sumdex/projects') {
return {
id,
name: null, // Auto-generated from context, or user-defined
host,
project,
connected: false,
active: false, // Claude session started
// messages: [], // Stored in separate sessionMessages state for performance
isProcessing: false,
error: null,
claudeSessionId: null,
stats: {
totalCost: 0,
inputTokens: 0,
outputTokens: 0,
cacheReadTokens: 0,
cacheCreationTokens: 0,
numTurns: 0,
},
permissionMode: 'default',
controlInitialized: false,
pendingPermission: null,
unreadCount: 0,
currentContext: null, // For dynamic tab naming
resumeOnStart: true,
};
}
const SessionContext = createContext(null);
export function SessionProvider({ children }) {
// All sessions keyed by ID (excludes messages for performance)
const [sessions, setSessions] = useState({});
// Messages stored separately to avoid re-rendering entire context on every message
const [sessionMessages, setSessionMessages] = useState({});
// Currently focused session ID
const [focusedSessionId, setFocusedSessionId] = useState(null);
// Sessions shown in split view (array of session IDs)
const [splitSessions, setSplitSessions] = useState([]);
// Tab order (array of session IDs)
const [tabOrder, setTabOrder] = useState([]);
// WebSocket refs keyed by session ID
const wsRefs = useRef({});
// Current assistant message refs keyed by session ID
const currentAssistantMessages = useRef({});
// Track if initial load is done (for auto-connecting restored sessions)
const initialLoadDone = useRef(false);
const sessionsToConnect = useRef([]);
// Load sessions from localStorage on mount
useEffect(() => {
try {
const stored = localStorage.getItem(SESSIONS_STORAGE_KEY);
if (stored) {
const data = JSON.parse(stored);
// Restore sessions but reset connection state
const restored = {};
const toConnect = [];
Object.entries(data.sessions || {}).forEach(([id, session]) => {
restored[id] = {
...session,
connected: false,
active: false,
isProcessing: false,
pendingPermission: null,
controlInitialized: false,
};
toConnect.push(id);
});
setSessions(restored);
setTabOrder(data.tabOrder || []);
setSplitSessions(data.splitSessions || []);
setFocusedSessionId(data.focusedSessionId || null);
sessionsToConnect.current = toConnect;
}
initialLoadDone.current = true;
} catch (e) {
console.error('Failed to restore sessions:', e);
initialLoadDone.current = true;
}
}, []);
// Save sessions to localStorage on change (debounced, excluding messages)
const saveTimeoutRef = useRef(null);
useEffect(() => {
// Debounce saves to avoid performance issues during streaming
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
saveTimeoutRef.current = setTimeout(() => {
try {
// Only save essential config, not transient state
const sessionsToSave = {};
Object.entries(sessions).forEach(([id, session]) => {
sessionsToSave[id] = {
id: session.id,
name: session.name,
host: session.host,
project: session.project,
permissionMode: session.permissionMode,
resumeOnStart: session.resumeOnStart,
};
});
const data = {
sessions: sessionsToSave,
tabOrder,
splitSessions,
focusedSessionId,
};
localStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(data));
} catch (e) {
console.error('Failed to save sessions:', e);
}
}, 1000);
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, [sessions, tabOrder, splitSessions, focusedSessionId]);
// Update a specific session
const updateSession = useCallback((sessionId, updates) => {
setSessions(prev => {
if (!prev[sessionId]) return prev;
return {
...prev,
[sessionId]: {
...prev[sessionId],
...(typeof updates === 'function' ? updates(prev[sessionId]) : updates),
},
};
});
}, []);
// Add message to session (uses separate state for performance)
const addMessage = useCallback((sessionId, message) => {
setSessionMessages(prev => ({
...prev,
[sessionId]: [...(prev[sessionId] || []), { ...message, timestamp: Date.now() }],
}));
// Update unread count in session state only if not focused
const isFocused = sessionId === focusedSessionId || splitSessions.includes(sessionId);
if (!isFocused) {
setSessions(prev => {
if (!prev[sessionId]) return prev;
return {
...prev,
[sessionId]: {
...prev[sessionId],
unreadCount: (prev[sessionId].unreadCount || 0) + 1,
},
};
});
}
}, [focusedSessionId, splitSessions]);
// Throttled streaming update refs
const pendingStreamUpdates = useRef({});
const streamUpdateTimers = useRef({});
// Update last assistant message (for streaming) - throttled to reduce re-renders
const updateLastAssistantMessage = useCallback((sessionId, content) => {
// Store the latest content
pendingStreamUpdates.current[sessionId] = content;
// If no timer is running, start one
if (!streamUpdateTimers.current[sessionId]) {
streamUpdateTimers.current[sessionId] = setTimeout(() => {
const latestContent = pendingStreamUpdates.current[sessionId];
delete pendingStreamUpdates.current[sessionId];
delete streamUpdateTimers.current[sessionId];
// Update only sessionMessages, not sessions (for performance)
setSessionMessages(prev => {
const messages = [...(prev[sessionId] || [])];
// Find last assistant message
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].type === 'assistant') {
messages[i] = { ...messages[i], content: latestContent };
break;
}
}
return {
...prev,
[sessionId]: messages,
};
});
}, 50); // Update UI max every 50ms
}
}, []);
// Handle Claude events (wrapped in claude_event messages from backend)
const handleClaudeEvent = useCallback((sessionId, event) => {
if (!event) return;
const { type } = event;
switch (type) {
case 'assistant': {
// Assistant message with content blocks (text + tool_use)
const message = event.message;
if (!message?.content) break;
const textBlocks = [];
const toolUseBlocks = [];
for (const block of message.content) {
if (block.type === 'text') {
textBlocks.push(block.text);
} else if (block.type === 'tool_use') {
toolUseBlocks.push({
type: 'tool_use',
tool: block.name,
input: block.input,
toolUseId: block.id,
});
}
}
// Add text content as assistant message
if (textBlocks.length > 0) {
// Check if we're streaming and should update existing message
if (currentAssistantMessages.current[sessionId] !== undefined) {
updateLastAssistantMessage(sessionId, textBlocks.join('\n'));
delete currentAssistantMessages.current[sessionId];
} else {
addMessage(sessionId, {
type: 'assistant',
content: textBlocks.join('\n'),
});
}
}
// Add tool use blocks as separate messages
for (const toolMsg of toolUseBlocks) {
addMessage(sessionId, toolMsg);
}
break;
}
case 'user': {
// User message with tool results
const message = event.message;
if (!message?.content) break;
for (const block of message.content) {
if (block.type === 'tool_result') {
addMessage(sessionId, {
type: 'tool_result',
toolUseId: block.tool_use_id,
content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
isError: block.is_error || false,
});
}
}
break;
}
case 'content_block_delta':
case 'stream_event': {
// Streaming text delta
const delta = event.delta || event;
if (delta?.type === 'text_delta' || delta?.text) {
const text = delta.text || '';
if (currentAssistantMessages.current[sessionId] === undefined) {
// Start new streaming message
currentAssistantMessages.current[sessionId] = text;
addMessage(sessionId, { type: 'assistant', content: text });
} else {
// Append to existing
currentAssistantMessages.current[sessionId] += text;
updateLastAssistantMessage(sessionId, currentAssistantMessages.current[sessionId]);
}
}
break;
}
case 'content_block_start': {
// Starting a new content block (text or tool_use)
const contentBlock = event.content_block;
if (contentBlock?.type === 'text') {
// Initialize streaming for text
currentAssistantMessages.current[sessionId] = '';
addMessage(sessionId, { type: 'assistant', content: '' });
} else if (contentBlock?.type === 'tool_use') {
// Tool use start - we'll get the full thing in assistant message
}
break;
}
case 'content_block_stop': {
// Content block finished
if (currentAssistantMessages.current[sessionId] !== undefined) {
// Clean up streaming state - message is already updated
delete currentAssistantMessages.current[sessionId];
}
break;
}
case 'message_start': {
// Message is starting
updateSession(sessionId, { isProcessing: true });
break;
}
case 'message_stop': {
// Message is complete
if (currentAssistantMessages.current[sessionId] !== undefined) {
delete currentAssistantMessages.current[sessionId];
}
break;
}
case 'result': {
// Final result with stats
const defaultStats = { totalCost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, numTurns: 0 };
updateSession(sessionId, (session) => ({
isProcessing: false,
stats: {
...(session.stats || defaultStats),
totalCost: event.totalCost ?? session.stats?.totalCost ?? 0,
inputTokens: event.inputTokens ?? session.stats?.inputTokens ?? 0,
outputTokens: event.outputTokens ?? session.stats?.outputTokens ?? 0,
cacheReadTokens: event.cacheReadTokens ?? session.stats?.cacheReadTokens ?? 0,
cacheCreationTokens: event.cacheCreationTokens ?? session.stats?.cacheCreationTokens ?? 0,
numTurns: event.numTurns ?? session.stats?.numTurns ?? 0,
},
currentContext: event.currentContext || session.currentContext,
}));
break;
}
default:
console.log(`[${sessionId}] Unhandled claude event:`, type, event);
}
}, [updateSession, addMessage, updateLastAssistantMessage]);
// Handle incoming WebSocket message for a session
const handleWsMessage = useCallback((sessionId, data) => {
const { type } = data;
switch (type) {
case 'session_started':
updateSession(sessionId, {
active: true,
claudeSessionId: data.sessionId,
error: null,
});
break;
case 'session_stopped':
updateSession(sessionId, {
active: false,
isProcessing: false,
});
break;
case 'control_initialized': {
updateSession(sessionId, {
controlInitialized: true,
});
// Restore saved permission mode after control is initialized
// Read from localStorage since sessions state may be stale in this callback
try {
const stored = localStorage.getItem(SESSIONS_STORAGE_KEY);
if (stored) {
const savedData = JSON.parse(stored);
const savedSession = savedData.sessions?.[sessionId];
if (savedSession?.permissionMode && savedSession.permissionMode !== 'default') {
const ws = wsRefs.current[sessionId];
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'set_permission_mode', mode: savedSession.permissionMode }));
}
}
}
} catch (e) {
console.error('Failed to restore permission mode:', e);
}
break;
}
case 'permission_mode_changed':
updateSession(sessionId, {
permissionMode: data.mode,
});
break;
case 'permission_request':
updateSession(sessionId, {
pendingPermission: data,
});
break;
case 'permission_resolved':
updateSession(sessionId, {
pendingPermission: null,
});
break;
case 'assistant_message':
if (data.partial) {
// Streaming update
if (!currentAssistantMessages.current[sessionId]) {
currentAssistantMessages.current[sessionId] = '';
addMessage(sessionId, { type: 'assistant', content: '' });
}
currentAssistantMessages.current[sessionId] = data.content;
updateLastAssistantMessage(sessionId, data.content);
} else {
// Final message
if (currentAssistantMessages.current[sessionId] !== undefined) {
updateLastAssistantMessage(sessionId, data.content);
delete currentAssistantMessages.current[sessionId];
} else {
addMessage(sessionId, { type: 'assistant', content: data.content });
}
}
break;
case 'user_message':
addMessage(sessionId, {
type: 'user',
content: data.content,
attachments: data.attachments,
});
break;
case 'tool_use':
addMessage(sessionId, {
type: 'tool_use',
tool: data.tool,
input: data.input,
toolUseId: data.toolUseId,
});
break;
case 'tool_result':
addMessage(sessionId, {
type: 'tool_result',
content: data.content,
toolUseId: data.toolUseId,
isError: data.isError,
});
break;
case 'processing':
updateSession(sessionId, { isProcessing: data.isProcessing });
break;
case 'result': {
const defaultStats = { totalCost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, numTurns: 0 };
updateSession(sessionId, (session) => ({
isProcessing: false,
stats: {
...(session.stats || defaultStats),
totalCost: data.totalCost ?? session.stats?.totalCost ?? 0,
inputTokens: data.inputTokens ?? session.stats?.inputTokens ?? 0,
outputTokens: data.outputTokens ?? session.stats?.outputTokens ?? 0,
cacheReadTokens: data.cacheReadTokens ?? session.stats?.cacheReadTokens ?? 0,
cacheCreationTokens: data.cacheCreationTokens ?? session.stats?.cacheCreationTokens ?? 0,
numTurns: data.numTurns ?? session.stats?.numTurns ?? 0,
},
currentContext: data.currentContext || session.currentContext,
}));
break;
}
case 'error':
updateSession(sessionId, {
error: data.message,
isProcessing: false,
});
addMessage(sessionId, { type: 'error', content: data.message });
break;
case 'history':
if (data.messages && Array.isArray(data.messages)) {
setSessionMessages(prev => ({
...prev,
[sessionId]: data.messages,
}));
}
break;
case 'session_ended':
updateSession(sessionId, {
active: false,
isProcessing: false,
});
addMessage(sessionId, {
type: 'system',
content: `Session ended (code: ${data.code})`,
});
break;
case 'claude_event':
handleClaudeEvent(sessionId, data.event);
break;
case 'generation_stopped':
updateSession(sessionId, { isProcessing: false });
if (data.message) {
addMessage(sessionId, { type: 'system', content: data.message });
}
break;
default:
console.log(`[${sessionId}] Unhandled message type:`, type, data);
}
}, [updateSession, addMessage, updateLastAssistantMessage, handleClaudeEvent]);
// Connect WebSocket for a session
const connectSession = useCallback((sessionId) => {
if (wsRefs.current[sessionId]?.readyState === WebSocket.OPEN) return;
const ws = new WebSocket(WS_URL);
wsRefs.current[sessionId] = ws;
ws.onopen = () => {
console.log(`[${sessionId}] WebSocket connected`);
updateSession(sessionId, { connected: true, error: null });
};
ws.onclose = () => {
console.log(`[${sessionId}] WebSocket disconnected`);
updateSession(sessionId, {
connected: false,
active: false,
isProcessing: false,
});
delete wsRefs.current[sessionId];
};
ws.onerror = (err) => {
console.error(`[${sessionId}] WebSocket error:`, err);
updateSession(sessionId, { error: 'Connection error' });
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleWsMessage(sessionId, data);
} catch (e) {
console.error(`[${sessionId}] Failed to parse message:`, e);
}
};
}, [updateSession, handleWsMessage]);
// Auto-connect restored sessions
useEffect(() => {
if (sessionsToConnect.current.length > 0) {
const toConnect = [...sessionsToConnect.current];
sessionsToConnect.current = [];
toConnect.forEach(sessionId => {
connectSession(sessionId);
});
}
}, [connectSession]);
// Disconnect WebSocket for a session
const disconnectSession = useCallback((sessionId) => {
const ws = wsRefs.current[sessionId];
if (ws) {
ws.close();
delete wsRefs.current[sessionId];
}
}, []);
// Create a new session
const createSession = useCallback((host = 'neko', project = '/home/sumdex/projects') => {
const id = generateSessionId();
const session = createSessionState(id, host, project);
setSessions(prev => ({ ...prev, [id]: session }));
setTabOrder(prev => [...prev, id]);
setFocusedSessionId(id);
// Auto-connect
setTimeout(() => connectSession(id), 0);
return id;
}, [connectSession]);
// Close a session (stops Claude but keeps in tabs unless removed)
const closeSession = useCallback((sessionId) => {
const ws = wsRefs.current[sessionId];
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'stop_session' }));
}
disconnectSession(sessionId);
}, [disconnectSession]);
// Remove session completely
const removeSession = useCallback((sessionId) => {
closeSession(sessionId);
setSessions(prev => {
const { [sessionId]: removed, ...rest } = prev;
return rest;
});
setTabOrder(prev => prev.filter(id => id !== sessionId));
setSplitSessions(prev => prev.filter(id => id !== sessionId));
// Focus another session if this was focused
setFocusedSessionId(prev => {
if (prev === sessionId) {
const remaining = tabOrder.filter(id => id !== sessionId);
return remaining[0] || null;
}
return prev;
});
}, [closeSession, tabOrder]);
// Start Claude session
const startClaudeSession = useCallback(async (sessionId) => {
const session = sessions[sessionId];
if (!session) return;
const ws = wsRefs.current[sessionId];
if (!ws || ws.readyState !== WebSocket.OPEN) {
connectSession(sessionId);
// Wait for connection
await new Promise(resolve => setTimeout(resolve, 500));
}
const currentWs = wsRefs.current[sessionId];
if (currentWs?.readyState === WebSocket.OPEN) {
// Load history if resuming
if (session.resumeOnStart) {
try {
const res = await fetch(
`${API_URL}/api/history/${encodeURIComponent(session.project)}?host=${session.host}`
);
const data = await res.json();
if (data.messages && Array.isArray(data.messages)) {
setSessionMessages(prev => ({
...prev,
[sessionId]: data.messages,
}));
}
} catch (e) {
console.error('Failed to load history:', e);
}
}
currentWs.send(JSON.stringify({
type: 'start_session',
project: session.project,
resume: session.resumeOnStart,
host: session.host,
}));
}
}, [sessions, connectSession, updateSession]);
// Stop Claude session
const stopClaudeSession = useCallback((sessionId) => {
const ws = wsRefs.current[sessionId];
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'stop_session' }));
}
updateSession(sessionId, { active: false, isProcessing: false });
}, [updateSession]);
// Send message to session
const sendMessage = useCallback(async (sessionId, message, attachments = []) => {
const session = sessions[sessionId];
if (!session?.active) return;
const ws = wsRefs.current[sessionId];
if (!ws || ws.readyState !== WebSocket.OPEN) return;
// Handle file uploads if needed
let uploadedFiles = [];
if (attachments.length > 0) {
const formData = new FormData();
for (const file of attachments) {
formData.append('files', file.file);
}
try {
const res = await fetch(`${API_URL}/api/upload/${session.claudeSessionId}`, {
method: 'POST',
body: formData,
});
const data = await res.json();
uploadedFiles = data.files || [];
} catch (e) {
console.error('Upload failed:', e);
updateSession(sessionId, { error: `Upload failed: ${e.message}` });
return;
}
}
// Build message with file references
let finalMessage = message;
if (uploadedFiles.length > 0) {
const fileRefs = uploadedFiles.map(f =>
f.isImage ? `[Image: ${f.path}]` : `[File: ${f.path}]`
).join('\n');
finalMessage = `${fileRefs}\n\n${message}`;
}
// Add user message to local state immediately for instant feedback
addMessage(sessionId, {
type: 'user',
content: message,
attachments: uploadedFiles.length > 0 ? uploadedFiles : undefined,
});
updateSession(sessionId, { isProcessing: true });
ws.send(JSON.stringify({
type: 'user_message',
message: finalMessage,
}));
}, [sessions, updateSession, addMessage]);
// Stop generation
const stopGeneration = useCallback((sessionId) => {
const ws = wsRefs.current[sessionId];
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'stop_generation' }));
}
updateSession(sessionId, { isProcessing: false });
}, [updateSession]);
// Clear messages
const clearMessages = useCallback((sessionId) => {
setSessionMessages(prev => ({
...prev,
[sessionId]: [],
}));
updateSession(sessionId, { unreadCount: 0 });
}, [updateSession]);
// Change permission mode
const changePermissionMode = useCallback((sessionId, mode) => {
const ws = wsRefs.current[sessionId];
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'set_permission_mode', mode }));
}
}, []);
// Respond to permission request
const respondToPermission = useCallback((sessionId, requestId, allow) => {
const ws = wsRefs.current[sessionId];
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'permission_response',
requestId,
allow,
}));
}
updateSession(sessionId, { pendingPermission: null });
}, [updateSession]);
// Reorder tabs
const reorderTabs = useCallback((fromIndex, toIndex) => {
setTabOrder(prev => {
const result = [...prev];
const [removed] = result.splice(fromIndex, 1);
result.splice(toIndex, 0, removed);
return result;
});
}, []);
// Add session to split view
const addToSplit = useCallback((sessionId) => {
setSplitSessions(prev => {
if (prev.includes(sessionId)) return prev;
if (prev.length >= 4) return prev; // Max 4 panels
return [...prev, sessionId];
});
}, []);
// Remove session from split view
const removeFromSplit = useCallback((sessionId) => {
setSplitSessions(prev => prev.filter(id => id !== sessionId));
}, []);
// Clear split view
const clearSplit = useCallback(() => {
setSplitSessions([]);
}, []);
// Mark session as read
const markAsRead = useCallback((sessionId) => {
updateSession(sessionId, { unreadCount: 0 });
}, [updateSession]);
// Rename session
const renameSession = useCallback((sessionId, name) => {
updateSession(sessionId, { name });
}, [updateSession]);
// Update session config (host, project, resume)
const updateSessionConfig = useCallback((sessionId, config) => {
updateSession(sessionId, config);
}, [updateSession]);
// Memoize focused session to prevent unnecessary re-renders
const focusedSession = useMemo(() => {
return focusedSessionId ? sessions[focusedSessionId] : null;
}, [focusedSessionId, sessions]);
// Memoize the entire context value to prevent re-renders when nothing changed
const value = useMemo(() => ({
// State
sessions,
sessionMessages,
tabOrder,
splitSessions,
focusedSessionId,
focusedSession,
// Session management
createSession,
closeSession,
removeSession,
renameSession,
updateSessionConfig,
// Focus & view
setFocusedSessionId,
markAsRead,
reorderTabs,
addToSplit,
removeFromSplit,
clearSplit,
// Claude session control
connectSession,
disconnectSession,
startClaudeSession,
stopClaudeSession,
// Messaging
sendMessage,
stopGeneration,
clearMessages,
// Permissions
changePermissionMode,
respondToPermission,
}), [
sessions, sessionMessages, tabOrder, splitSessions, focusedSessionId, focusedSession,
createSession, closeSession, removeSession, renameSession, updateSessionConfig,
setFocusedSessionId, markAsRead, reorderTabs, addToSplit, removeFromSplit, clearSplit,
connectSession, disconnectSession, startClaudeSession, stopClaudeSession,
sendMessage, stopGeneration, clearMessages, changePermissionMode, respondToPermission,
]);
return (
<SessionContext.Provider value={value}>
{children}
</SessionContext.Provider>
);
}
export function useSessionManager() {
const context = useContext(SessionContext);
if (!context) {
throw new Error('useSessionManager must be used within SessionProvider');
}
return context;
}
// Hook to get a specific session's data and actions
export function useSession(sessionId) {
const manager = useSessionManager();
const session = manager.sessions[sessionId];
const messages = manager.sessionMessages[sessionId] || [];
// Combine session data with messages for backward compatibility
const sessionWithMessages = session ? { ...session, messages } : null;
return {
session: sessionWithMessages,
messages, // Also expose separately for components that only need messages
start: () => manager.startClaudeSession(sessionId),
stop: () => manager.stopClaudeSession(sessionId),
send: (msg, attachments) => manager.sendMessage(sessionId, msg, attachments),
stopGeneration: () => manager.stopGeneration(sessionId),
clearMessages: () => manager.clearMessages(sessionId),
changePermissionMode: (mode) => manager.changePermissionMode(sessionId, mode),
respondToPermission: (reqId, allow) => manager.respondToPermission(sessionId, reqId, allow),
close: () => manager.closeSession(sessionId),
remove: () => manager.removeSession(sessionId),
rename: (name) => manager.renameSession(sessionId, name),
updateConfig: (config) => manager.updateSessionConfig(sessionId, config),
addToSplit: () => manager.addToSplit(sessionId),
removeFromSplit: () => manager.removeFromSplit(sessionId),
focus: () => manager.setFocusedSessionId(sessionId),
markAsRead: () => manager.markAsRead(sessionId),
};
}