import { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo } from 'react'; // Build WebSocket URL from current location function getWsUrl() { if (import.meta.env.VITE_WS_URL) return import.meta.env.VITE_WS_URL; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws`; } 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, availableCommands: [], // Slash commands from Claude 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({}); // Ref to current sessions state (for stable callbacks) const sessionsRef = useRef(sessions); sessionsRef.current = sessions; // 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; const newUpdates = typeof updates === 'function' ? updates(prev[sessionId]) : updates; return { ...prev, [sessionId]: { ...prev[sessionId], ...newUpdates, }, }; }); }, []); // 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); } // Extract usage stats from message if present const usage = message.usage; if (usage) { const inputTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0); const outputTokens = usage.output_tokens || 0; const cacheReadTokens = usage.cache_read_input_tokens || 0; const cacheCreationTokens = usage.cache_creation_input_tokens || 0; updateSession(sessionId, (session) => ({ stats: { ...(session.stats || {}), inputTokens: (session.stats?.inputTokens || 0) + inputTokens, outputTokens: (session.stats?.outputTokens || 0) + outputTokens, cacheReadTokens: (session.stats?.cacheReadTokens || 0) + cacheReadTokens, cacheCreationTokens: (session.stats?.cacheCreationTokens || 0) + cacheCreationTokens, numTurns: (session.stats?.numTurns || 0) + 1, }, })); } 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 console.log(`[${sessionId}] Result event:`, JSON.stringify(event, null, 2)); 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; } case 'system': { // System events from Claude - log full event for debugging console.log(`[${sessionId}] System event:`, event.subtype, JSON.stringify(event, null, 2)); // Check for context info in system init event if (event.subtype === 'init') { // Log all fields to find context info console.log(`[${sessionId}] System init fields:`, Object.keys(event)); } // Parse context message if present if (event.message) { const contextMatch = event.message.match(/Context left[^:]*:\s*(\d+)%/i); if (contextMatch) { const contextLeft = parseInt(contextMatch[1], 10); console.log(`[${sessionId}] Context left:`, contextLeft + '%'); updateSession(sessionId, (session) => ({ stats: { ...(session.stats || {}), contextLeftPercent: contextLeft, }, })); } } 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, availableCommands: data.commands || [], }); // 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; case 'context_update': // Live context window metrics from message_delta events updateSession(sessionId, (session) => ({ stats: { ...(session.stats || {}), tokensUsed: data.tokensUsed, contextWindow: data.contextWindow, contextLeftPercent: data.percentRemaining, }, })); break; case 'compacting_started': console.log(`[${sessionId}] Compacting started`); updateSession(sessionId, (session) => ({ stats: { ...(session.stats || {}), isCompacting: true, }, })); addMessage(sessionId, { type: 'system', content: '⏳ Context compacting in progress...', }); break; case 'compacting_finished': console.log(`[${sessionId}] Compacting finished`); updateSession(sessionId, (session) => ({ stats: { ...(session.stats || {}), isCompacting: false, }, })); break; case 'compact_boundary': console.log(`[${sessionId}] Compact boundary:`, data); // Context was compacted - reset the metrics updateSession(sessionId, (session) => ({ stats: { ...(session.stats || {}), isCompacting: false, tokensUsed: 0, contextLeftPercent: 100, lastCompactTrigger: data.trigger, lastCompactPreTokens: data.preTokens, }, })); addMessage(sessionId, { type: 'system', content: `✅ Context compacted (${data.trigger}). Was at ${data.percentUsedBeforeCompact}% usage.`, }); 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(getWsUrl()); 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 = sessionsRef.current[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/history/${encodeURIComponent(session.project)}?host=${session.host}`, { credentials: 'include' } ); 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, })); } }, [connectSession]); // 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 = sessionsRef.current[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/upload/${session.claudeSessionId}`, { method: 'POST', body: formData, credentials: 'include', }); 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, })); }, [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]); // Set compacting state const setCompacting = useCallback((sessionId, value) => { updateSession(sessionId, { stats: { ...(sessions[sessionId]?.stats || {}), isCompacting: value, }, }); }, [updateSession, sessions]); // 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, setCompacting, // 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, setCompacting, changePermissionMode, respondToPermission, ]); return ( {children} ); } 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), }; }