Enhance CLI Stream Viewer and Navigation Lifecycle Management

- Added lifecycle management for CLI Stream Viewer with destroy function to clean up event listeners and timers.
- Improved navigation state management by registering destroy functions for views and ensuring cleanup on transitions.
- Updated Claude Manager to include lifecycle functions for better resource management.
- Enhanced CLI History View with state reset functionality and improved dropdown handling for batch delete.
- Introduced round solutions rendering in Lite Tasks View, including collapsible sections for implementation plans, dependencies, and technical concerns.
This commit is contained in:
catlog22
2026-01-14 19:57:05 +08:00
parent 49845fe1ae
commit 959d60b31f
6 changed files with 837 additions and 60 deletions

View File

@@ -3,6 +3,12 @@
* Real-time streaming output viewer for CLI executions
*/
// ===== Lifecycle Management =====
let cliStreamViewerDestroy = null;
let streamKeyboardHandler = null;
let streamScrollHandler = null; // Track scroll listener
let streamStatusTimers = []; // Track status update timers
// ===== State Management =====
let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } }
let activeStreamTab = null;
@@ -91,7 +97,7 @@ async function syncActiveExecutions() {
// ===== Initialization =====
function initCliStreamViewer() {
// Initialize keyboard shortcuts
document.addEventListener('keydown', function(e) {
streamKeyboardHandler = function(e) {
if (e.key === 'Escape' && isCliStreamViewerOpen) {
if (searchFilter) {
clearSearch();
@@ -108,12 +114,14 @@ function initCliStreamViewer() {
searchInput.select();
}
}
});
};
document.addEventListener('keydown', streamKeyboardHandler);
// Initialize scroll detection for auto-scroll
const content = document.getElementById('cliStreamContent');
if (content) {
content.addEventListener('scroll', handleStreamContentScroll);
streamScrollHandler = handleStreamContentScroll;
content.addEventListener('scroll', streamScrollHandler);
}
// Sync active executions from server (recover state for mid-execution joins)
@@ -592,11 +600,12 @@ function renderStreamStatus(executionId) {
// Update duration periodically for running executions
if (exec.status === 'running') {
setTimeout(() => {
const timerId = setTimeout(() => {
if (activeStreamTab === executionId && cliStreamExecutions[executionId]?.status === 'running') {
renderStreamStatus(executionId);
}
}, 1000);
streamStatusTimers.push(timerId);
}
}
@@ -760,6 +769,31 @@ if (document.readyState === 'loading') {
initCliStreamViewer();
}
// ===== Lifecycle Functions =====
function destroyCliStreamViewer() {
// Remove keyboard event listener if exists
if (streamKeyboardHandler) {
document.removeEventListener('keydown', streamKeyboardHandler);
streamKeyboardHandler = null;
}
// Remove scroll event listener if exists
if (streamScrollHandler) {
const content = document.getElementById('cliStreamContent');
if (content) {
content.removeEventListener('scroll', streamScrollHandler);
}
streamScrollHandler = null;
}
// Clear all pending status update timers
streamStatusTimers.forEach(timerId => clearTimeout(timerId));
streamStatusTimers = [];
}
// Export lifecycle functions
window.destroyCliStreamViewer = destroyCliStreamViewer;
// ===== Global Exposure =====
window.toggleCliStreamViewer = toggleCliStreamViewer;
window.handleCliStreamStarted = handleCliStreamStarted;

View File

@@ -1,6 +1,9 @@
// Navigation and Routing
// Manages navigation events, active state, content title updates, search, and path selector
// View lifecycle management
var currentViewDestroy = null;
// Path Selector
function initPathSelector() {
const btn = document.getElementById('pathButton');
@@ -54,6 +57,11 @@ function initPathSelector() {
// Cleanup function for view transitions
function cleanupPreviousView() {
// Call current view's destroy function if exists
if (currentViewDestroy) {
currentViewDestroy();
currentViewDestroy = null;
}
// Cleanup graph explorer
if (currentView === 'graph-explorer' && typeof window.cleanupGraphExplorer === 'function') {
window.cleanupGraphExplorer();
@@ -118,9 +126,13 @@ function initNavigation() {
} else if (currentView === 'explorer') {
renderExplorer();
} else if (currentView === 'cli-manager') {
renderCliManager();
renderClaudeManager();
} else if (currentView === 'cli-history') {
renderCliHistoryView();
// Register destroy function for cli-history view
if (typeof window.destroyCliHistoryView === 'function') {
currentViewDestroy = window.destroyCliHistoryView;
}
} else if (currentView === 'hook-manager') {
renderHookManager();
} else if (currentView === 'memory') {
@@ -133,6 +145,10 @@ function initNavigation() {
renderRulesManager();
} else if (currentView === 'claude-manager') {
renderClaudeManager();
// Register destroy function for claude-manager view
if (typeof window.initClaudeManager === 'function') {
window.initClaudeManager();
}
} else if (currentView === 'graph-explorer') {
renderGraphExplorer();
} else if (currentView === 'help') {

View File

@@ -1,6 +1,10 @@
// CLAUDE.md Manager View
// Three-column layout: File Tree | Viewer/Editor | Metadata & Actions
// ========== Lifecycle Management ==========
var claudeManagerDestroy = null;
var createDialogOverlay = null; // Track create dialog overlay for cleanup
// ========== State Management ==========
var claudeFilesData = {
user: { main: null },
@@ -19,9 +23,15 @@ var fileTreeExpanded = {
var searchQuery = '';
var freshnessData = {}; // { [filePath]: FreshnessResult }
var freshnessSummary = null;
var searchKeyboardHandlerAdded = false;
// ========== Main Render Function ==========
async function renderClaudeManager() {
// Initialize lifecycle
if (typeof initClaudeManager === 'function') {
initClaudeManager();
}
var container = document.getElementById('mainContent');
if (!container) return;
@@ -749,9 +759,11 @@ function filterFileTree(query) {
renderFileTree();
// Add keyboard shortcut handler
if (query && !window.claudeSearchKeyboardHandlerAdded) {
document.addEventListener('keydown', handleSearchKeyboard);
window.claudeSearchKeyboardHandlerAdded = true;
if (query && !searchKeyboardHandlerAdded) {
(function() {
document.addEventListener('keydown', handleSearchKeyboard);
searchKeyboardHandlerAdded = true;
})();
}
}
@@ -796,6 +808,7 @@ function showCreateFileDialog() {
'</div>';
document.body.insertAdjacentHTML('beforeend', dialog);
createDialogOverlay = document.querySelector('.modal-overlay');
if (window.lucide) lucide.createIcons();
}
@@ -918,3 +931,31 @@ function updateClaudeBadge() {
badge.textContent = total;
}
}
// ========== Lifecycle Functions ==========
function destroyClaudeManager() {
// Remove search keyboard event listener if added
if (searchKeyboardHandlerAdded) {
document.removeEventListener('keydown', handleSearchKeyboard);
searchKeyboardHandlerAdded = false;
}
// Remove create dialog overlay if exists
if (createDialogOverlay) {
createDialogOverlay.remove();
createDialogOverlay = null;
}
// Reset view-specific state
selectedFile = null;
isEditMode = false;
isDirty = false;
}
// Expose init/destroy functions for lifecycle management
window.initClaudeManager = function() {
claudeManagerDestroy = destroyClaudeManager;
};
// Make destroyClaudeManager accessible globally as well
window.destroyClaudeManager = destroyClaudeManager;

View File

@@ -7,6 +7,9 @@ var isMultiSelectMode = false;
// ========== Rendering ==========
async function renderCliHistoryView() {
// Reset view state to prevent stale data persistence
resetHistoryViewState();
var container = document.getElementById('mainContent');
if (!container) return;
@@ -183,6 +186,9 @@ async function renderCliHistoryView() {
if (window.lucide) lucide.createIcons();
}
// Export destroy function for lifecycle management
window.destroyCliHistoryView = destroyCliHistoryView;
// ========== Actions ==========
async function copyExecutionId(executionId) {
try {
@@ -218,16 +224,28 @@ async function refreshCliHistoryView() {
}
// ========== Multi-Select Functions ==========
var deleteDropdownListenerActive = false;
function toggleDeleteDropdown(event) {
event.stopPropagation();
var menu = document.getElementById('deleteDropdownMenu');
if (menu) {
menu.classList.toggle('show');
// Close on outside click
if (menu.classList.contains('show')) {
setTimeout(function() {
document.addEventListener('click', closeDeleteDropdown);
}, 0);
// Closing: remove listener if active
menu.classList.remove('show');
if (deleteDropdownListenerActive) {
document.removeEventListener('click', closeDeleteDropdown);
deleteDropdownListenerActive = false;
}
} else {
// Opening: add listener if not already active
menu.classList.add('show');
if (!deleteDropdownListenerActive) {
setTimeout(function() {
document.addEventListener('click', closeDeleteDropdown);
deleteDropdownListenerActive = true;
}, 0);
}
}
}
}
@@ -235,7 +253,10 @@ function toggleDeleteDropdown(event) {
function closeDeleteDropdown() {
var menu = document.getElementById('deleteDropdownMenu');
if (menu) menu.classList.remove('show');
document.removeEventListener('click', closeDeleteDropdown);
if (deleteDropdownListenerActive) {
document.removeEventListener('click', closeDeleteDropdown);
deleteDropdownListenerActive = false;
}
}
function enterMultiSelectMode() {
@@ -279,6 +300,23 @@ function clearExecutionSelection() {
renderCliHistoryView();
}
function resetHistoryViewState() {
selectedExecutions.clear();
isMultiSelectMode = false;
}
// ========== View Lifecycle ==========
function destroyCliHistoryView() {
// Clean up dropdown listener if active
if (deleteDropdownListenerActive) {
document.removeEventListener('click', closeDeleteDropdown);
deleteDropdownListenerActive = false;
}
// Ensure dropdown menu is closed
var menu = document.getElementById('deleteDropdownMenu');
if (menu) menu.classList.remove('show');
}
// ========== Batch Delete Functions ==========
function confirmBatchDelete() {
var count = selectedExecutions.size;

View File

@@ -1117,6 +1117,143 @@ function renderRoundContent(round) {
`);
}
// Round solutions
if (round.solutions && round.solutions.length > 0) {
sections.push(`
<div class="round-solutions">
<strong>${t('multiCli.solutions') || 'Solutions'} (${round.solutions.length}):</strong>
<div class="solutions-list">
${round.solutions.map((solution, idx) => `
<div class="solution-card">
<div class="solution-header">
<div class="solution-title">
<span class="solution-number">${idx + 1}</span>
<span class="solution-name">${escapeHtml(solution.name || `Solution ${idx + 1}`)}</span>
</div>
<div class="solution-meta">
${solution.source_cli?.length ? `
<div class="source-clis">
${solution.source_cli.map(cli => `<span class="cli-badge">${escapeHtml(cli)}</span>`).join('')}
</div>
` : ''}
<div class="solution-scores">
<span class="score-badge feasibility" title="Feasibility">
${Math.round((solution.feasibility || 0) * 100)}%
</span>
<span class="score-badge effort-${solution.effort || 'medium'}" title="Effort">
${escapeHtml(solution.effort || 'medium')}
</span>
<span class="score-badge risk-${solution.risk || 'medium'}" title="Risk">
${escapeHtml(solution.risk || 'medium')}
</span>
</div>
</div>
</div>
${solution.summary ? `
<div class="solution-summary">
${escapeHtml(getI18nText(solution.summary))}
</div>
` : ''}
${solution.implementation_plan?.approach ? `
<div class="solution-approach collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label">${t('multiCli.implementation') || 'Implementation Approach'}</span>
</div>
<div class="collapsible-content collapsed">
<p>${escapeHtml(getI18nText(solution.implementation_plan.approach))}</p>
${solution.implementation_plan.tasks?.length ? `
<div class="solution-tasks">
<strong>${t('multiCli.tasks') || 'Tasks'}:</strong>
<ul class="task-list">
${solution.implementation_plan.tasks.map(task => `
<li class="task-item">
<span class="task-id">${escapeHtml(task.id || '')}</span>
<span class="task-name">${escapeHtml(getI18nText(task.name))}</span>
${task.key_point ? `<span class="task-key-point">${escapeHtml(getI18nText(task.key_point))}</span>` : ''}
</li>
`).join('')}
</ul>
</div>
` : ''}
${solution.implementation_plan.execution_flow ? `
<div class="execution-flow">
<strong>${t('multiCli.executionFlow') || 'Execution Flow'}:</strong>
<code class="flow-code">${escapeHtml(solution.implementation_plan.execution_flow)}</code>
</div>
` : ''}
${solution.implementation_plan.milestones?.length ? `
<div class="solution-milestones">
<strong>${t('multiCli.milestones') || 'Milestones'}:</strong>
<ul class="milestone-list">
${solution.implementation_plan.milestones.map(milestone => `
<li class="milestone-item">${escapeHtml(getI18nText(milestone))}</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
` : ''}
${(solution.dependencies?.internal?.length || solution.dependencies?.external?.length) ? `
<div class="solution-dependencies collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label">${t('multiCli.dependencies') || 'Dependencies'}</span>
</div>
<div class="collapsible-content collapsed">
${solution.dependencies.internal?.length ? `
<div class="internal-deps">
<strong>${t('multiCli.internalDeps') || 'Internal'}:</strong>
<ul class="dep-list">
${solution.dependencies.internal.map(dep => `
<li class="dep-item">${escapeHtml(getI18nText(dep))}</li>
`).join('')}
</ul>
</div>
` : ''}
${solution.dependencies.external?.length ? `
<div class="external-deps">
<strong>${t('multiCli.externalDeps') || 'External'}:</strong>
<ul class="dep-list">
${solution.dependencies.external.map(dep => `
<li class="dep-item">${escapeHtml(getI18nText(dep))}</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
` : ''}
${solution.technical_concerns?.length ? `
<div class="solution-concerns collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">&#9658;</span>
<span class="section-label">${t('multiCli.technicalConcerns') || 'Technical Concerns'}</span>
</div>
<div class="collapsible-content collapsed">
<ul class="concern-list">
${solution.technical_concerns.map(concern => `
<li class="concern-item">${escapeHtml(getI18nText(concern))}</li>
`).join('')}
</ul>
</div>
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}