UNPKG

@calmhive/calmhive-cli

Version:

🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry

278 lines (242 loc) 6.92 kB
/** * ViewManager - Handles TUI layout, view management and rendering * * Extracted from TUIManager to provide focused responsibility for: * - Screen layout creation (header, content area, footer) * - View loading and initialization * - View switching and coordination * - Screen rendering and updates * - Footer status updates */ const blessed = require('blessed'); class ViewManager { constructor(tuiManager) { this.tuiManager = tuiManager; this.screen = null; this.header = null; this.contentArea = null; this.footer = null; this.views = {}; this.currentView = 'sessionList'; this._renderTimeout = null; } /** * Initialize the screen and set up the base layout structure */ initializeScreen() { // Create screen this.screen = blessed.screen({ smartCSR: true, fullUnicode: true, title: 'Calmhive Process Manager' }); this.setupLayout(); return this.screen; } /** * Set up the base layout structure */ setupLayout() { // Header this.header = blessed.box({ parent: this.screen, top: 0, left: 0, width: '100%', height: 3, content: '🐝 Calmhive V3 Process Manager', tags: true, border: { type: 'line' }, style: { fg: 'yellow', border: { fg: 'yellow' } } }); // Main content area this.contentArea = blessed.box({ parent: this.screen, top: 3, left: 0, width: '100%', height: '100%-6', tags: true, scrollable: true, alwaysScroll: true, mouse: true }); // Footer/status bar this.footer = blessed.box({ parent: this.screen, bottom: 0, left: 0, width: '100%', height: 3, content: 'Commands: n:New s:Stop r:Resume l:Logs c:Cleanup q:Quit', tags: true, border: { type: 'line' }, style: { fg: 'green', border: { fg: 'green' } } }); } /** * Load all views */ async loadViews() { // Import view modules const SessionListView = require('./views/session-list'); const SessionDetailView = require('./views/session-detail'); const LogsView = require('./views/logs-viewer'); // Initialize views this.views.sessionList = new SessionListView(this.contentArea, this.tuiManager); this.views.sessionDetail = new SessionDetailView(this.contentArea, this.tuiManager); this.views.logs = new LogsView(this.contentArea, this.tuiManager); // Hide all views initially Object.values(this.views).forEach(view => view.hide()); // Show default view this.views[this.currentView].show(); } /** * Switch between views */ switchView(viewName, data = null) { try { // Debug logging console.error(`[TUI DEBUG] Switching from ${this.currentView} to ${viewName}`); // Validate view exists if (!this.views[viewName]) { console.error(`[TUI ERROR] View '${viewName}' does not exist!`); this.tuiManager.showMessage(`Error: View '${viewName}' not found`, 'error'); return; } // Hide current view if (this.views[this.currentView] && this.views[this.currentView].hide) { this.views[this.currentView].hide(); } // Show new view this.currentView = viewName; // Call show with error handling if (this.views[this.currentView] && this.views[this.currentView].show) { const showResult = this.views[this.currentView].show(data); // Handle promise if returned if (showResult && showResult.then) { showResult.catch(error => { console.error(`[TUI ERROR] Error showing view ${viewName}:`, error); this.tuiManager.showMessage(`Error: ${error.message}`, 'error'); }); } } this.updateFooter(); // Ensure proper focus on the new view with immediate render setTimeout(() => { try { if (this.views[this.currentView] && this.views[this.currentView].listTable) { this.views[this.currentView].listTable.focus(); } else if (this.views[this.currentView] && this.views[this.currentView].logBox) { // For log viewer, focus on logBox this.views[this.currentView].logBox.focus(); } this.render(true); // Immediate render for view switches } catch (error) { console.error('[TUI ERROR] Error during focus:', error); } }, 10); } catch (error) { console.error(`[TUI ERROR] Error in switchView to ${viewName}:`, error); this.tuiManager.showMessage(`Error: ${error.message}`, 'error'); } } /** * Update footer content based on current view */ updateFooter() { const footerContent = { sessionList: 'Nav: ↑↓jk gg/G H/M Ctrl+u/d /? n/N | Commands: n:New s:Stop l:Logs c:Cleanup o:Orphans q:Quit', sessionDetail: 'Nav: ↑↓jk gg/G H/M Ctrl+u/d /? | Commands: ESC:Back s:Stop l:Logs q:Quit', logs: 'Nav: ↑↓jk gg/G H/M Ctrl+u/d /? n/N | Commands: ESC:Back q:Quit' }; this.footer.setContent(footerContent[this.currentView] || 'Press ESC to go back, q to quit'); } /** * Render the screen with optional immediate rendering */ render(immediate = false) { if (immediate) { // Immediate render for critical updates with full redraw this.screen.clearRegion(0, this.screen.width, 0, this.screen.height); this.screen.render(); return; } // Debounce renders for non-critical updates if (this._renderTimeout) { clearTimeout(this._renderTimeout); } this._renderTimeout = setTimeout(() => { this.screen.render(); this._renderTimeout = null; }, 16); // ~60fps max } /** * Force complete screen refresh */ forceRefresh() { this.screen.clearRegion(0, this.screen.width, 0, this.screen.height); this.screen.render(); } /** * Get the current view */ getCurrentView() { return this.currentView; } /** * Get a specific view by name */ getView(viewName) { return this.views[viewName]; } /** * Get all views */ getAllViews() { return this.views; } /** * Get the content area for views to attach to */ getContentArea() { return this.contentArea; } /** * Get the screen object */ getScreen() { return this.screen; } /** * Clean up ViewManager resources */ cleanup() { if (this._renderTimeout) { clearTimeout(this._renderTimeout); this._renderTimeout = null; } // Clean up views Object.values(this.views).forEach(view => { if (view.cleanup && typeof view.cleanup === 'function') { view.cleanup(); } }); this.views = {}; } } module.exports = ViewManager;