UNPKG

@calmhive/calmhive-cli

Version:

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

322 lines (276 loc) 7.81 kB
/** * Session Detail View * * Detailed view of a specific session with metadata and actions */ const blessed = require('blessed'); const SessionDatabase = require('../../session-database'); class SessionDetailView { constructor(parent, manager) { this.parent = parent; this.manager = manager; this.sessionDb = new SessionDatabase(); this.session = null; this.visible = false; this.createComponents(); } /** * Create view components */ createComponents() { // Main container this.container = blessed.box({ parent: this.parent, width: '100%', height: '100%', hidden: true }); // Title this.title = blessed.text({ parent: this.container, top: 0, left: 0, width: '100%', height: 1, content: '{center}Session Details{/center}', tags: true, style: { fg: 'yellow', bold: true } }); // Details box this.detailsBox = blessed.box({ parent: this.container, top: 2, left: 0, width: '100%', height: '70%', border: { type: 'line' }, style: { border: { fg: 'cyan' } }, scrollable: true, alwaysScroll: true, mouse: true, tags: true }); // Actions box this.actionsBox = blessed.box({ parent: this.container, bottom: 0, left: 0, width: '100%', height: '25%', border: { type: 'line' }, style: { border: { fg: 'green' } }, content: '{center}Actions{/center}\n\n' + ' [s] Stop Session\n' + ' [l] View Logs\n' + ' [ESC] Back to List', tags: true }); } /** * Show the view with session data */ async show(data) { this.visible = true; this.container.show(); if (data && data.session) { this.session = data.session; await this.updateDetails(); } } /** * Hide the view */ hide() { this.visible = false; this.container.hide(); } /** * Update session details */ async updateDetails() { if (!this.session) {return;} let content = ''; // Basic information content += `{bold}Session ID:{/bold} ${this.session.id}\n`; content += `{bold}Type:{/bold} ${(this.session.type || 'unknown').toUpperCase()}\n`; content += `{bold}Status:{/bold} ${this.formatStatus(this.session.status)}\n`; content += `{bold}Task:{/bold} ${this.session.task}\n`; content += '\n'; // Timing information content += `{bold}Started:{/bold} ${new Date(this.session.started_at).toLocaleString()}\n`; content += `{bold}Updated:{/bold} ${new Date(this.session.updated_at).toLocaleString()}\n`; if (this.session.completed_at) { content += `{bold}Completed:{/bold} ${new Date(this.session.completed_at).toLocaleString()}\n`; } content += `{bold}Duration:{/bold} ${this.formatDuration(this.session)}\n`; content += '\n'; // Process information if (this.session.pid) { content += `{bold}Process ID:{/bold} ${this.session.pid}\n`; } content += `{bold}Working Directory:{/bold} ${this.session.working_directory}\n`; content += '\n'; // AFk-specific information if (this.session.type === 'afk') { content += `{bold}Iterations:{/bold} ${this.session.iterations_completed || 0} / ${this.session.iterations_planned || 1}\n`; const percentage = Math.round(((this.session.iterations_completed || 0) / (this.session.iterations_planned || 1)) * 100); content += `{bold}Progress:{/bold} ${this.createProgressBar(percentage, 30)} ${percentage}%\n`; } // Metadata if (this.session.metadata) { content += '\n{bold}Metadata:{/bold}\n'; try { const metadata = typeof this.session.metadata === 'string' ? JSON.parse(this.session.metadata) : this.session.metadata; for (const [key, value] of Object.entries(metadata)) { content += ` {cyan-fg}${key}:{/} ${JSON.stringify(value)}\n`; } } catch (e) { content += ` ${this.session.metadata}\n`; } } // Recent logs preview if (this.session.logs) { content += '\n{bold}Recent Logs:{/bold}\n'; const logLines = this.session.logs.split('\n').slice(-10); logLines.forEach(line => { content += ` {gray-fg}${this.truncate(line, 70)}{/}\n`; }); } this.detailsBox.setContent(content); this.manager.render(); } /** * Format status with color */ formatStatus(status) { const statusColors = { 'running': '{green-fg}▶ Running{/}', 'completed': '{cyan-fg}✓ Completed{/}', 'error': '{red-fg}✗ Error{/}', 'stopped': '{yellow-fg}■ Stopped{/}' }; return statusColors[status] || status; } /** * Format duration */ formatDuration(session) { const start = session.started_at; const end = session.completed_at || Date.now(); if (!start) {return '-';} const duration = end - start; const seconds = Math.floor(duration / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } else if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } /** * Create a progress bar */ createProgressBar(percentage, width) { const filled = Math.round((percentage / 100) * width); const empty = width - filled; return '{green-fg}' + '█'.repeat(filled) + '{/}' + '░'.repeat(empty); } /** * Truncate string */ truncate(str, length) { if (!str) {return '';} if (str.length <= length) {return str;} return str.substring(0, length - 3) + '...'; } /** * Navigation methods for detail view */ navigateUp() { this.detailsBox.scroll(-1); } navigateDown() { this.detailsBox.scroll(1); } pageUp() { this.detailsBox.scroll(-10); } pageDown() { this.detailsBox.scroll(10); } /** * Enhanced vim-style navigation methods */ goToTop() { this.detailsBox.setScrollPerc(0); } goToBottom() { this.detailsBox.setScrollPerc(100); } goToMiddle() { // Go to middle of content (what M should do) this.detailsBox.setScrollPerc(50); } goToViewTop() { // Already at view top for detail view } goToViewMiddle() { this.detailsBox.setScrollPerc(50); } goToViewBottom() { this.detailsBox.setScrollPerc(100); } halfPageUp() { const halfPage = Math.floor((this.detailsBox.height - 2) / 2); this.detailsBox.scroll(-halfPage); } halfPageDown() { const halfPage = Math.floor((this.detailsBox.height - 2) / 2); this.detailsBox.scroll(halfPage); } /** * Search functionality for session detail */ search(searchTerm, direction = 'forward') { if (!searchTerm || !this.session) {return;} this.currentSearchTerm = searchTerm.toLowerCase(); const content = this.detailsBox.getContent().toLowerCase(); if (content.includes(this.currentSearchTerm)) { this.manager.showMessage(`Found: ${searchTerm}`, 'info'); } else { this.manager.showMessage(`Not found: ${searchTerm}`, 'warning'); } } nextSearchResult() { // Simple implementation for detail view this.manager.showMessage('Use / to search in detail view', 'info'); } previousSearchResult() { // Simple implementation for detail view this.manager.showMessage('Use / to search in detail view', 'info'); } } module.exports = SessionDetailView;