diff --git a/app.py b/app.py index ba877de..63337fc 100644 --- a/app.py +++ b/app.py @@ -17,10 +17,10 @@ import shutil import logging import urllib.error -from datetime import datetime, timezone -from pathlib import Path -from flask import Flask, request, jsonify, send_from_directory, Response -from werkzeug.exceptions import BadRequest +from datetime import datetime, timezone +from pathlib import Path +from flask import Flask, request, jsonify, send_from_directory, Response +from werkzeug.exceptions import BadRequest # Setup logger for DevShell backend logging logging.basicConfig(level=logging.INFO) @@ -2397,18 +2397,19 @@ def parse_script_metadata(filepath): with open(filepath, "r", encoding="utf-8", errors="replace") as f: for line in f: line = line.strip() - if line.startswith("# name:"): - name_val = line[7:].strip() - if name_val: - metadata["name"] = name_val - elif line.startswith("# desc:"): - metadata["desc"] = line[7:].strip() - elif line.startswith("# tag:"): - metadata["tag"] = line[6:].strip() - elif line.startswith("# url:"): - metadata["url"] = line[6:].strip() - elif not line.startswith("#") and line: + if line.startswith('# name:'): + name_val = line[7:].strip() + if name_val: + metadata['name'] = name_val + elif line.startswith('# desc:'): + metadata['desc'] = line[7:].strip() + elif line.startswith('# tag:'): + metadata['tag'] = line[6:].strip() + elif line.startswith('# url:'): + metadata['url'] = line[6:].strip() + elif not line.startswith('#') and line: break + except Exception: # nosec B110 pass return metadata @@ -2450,10 +2451,10 @@ def get_all_scripts(): # ─── Security Enhancements ────────────────────────────────────────── @app.before_request -def enforce_security(): - from flask import abort - from urllib.parse import urlparse - +def enforce_security(): + from flask import abort + from urllib.parse import urlparse + # 1. Host Validation (prevents DNS Rebinding) host_only = request.host.split(':')[0] if host_only not in ('127.0.0.1', 'localhost'): @@ -2479,22 +2480,22 @@ def is_valid_local(url): abort(403) else: # Reject if neither is present and request is from a browser - user_agent = request.headers.get('User-Agent', '') - if any(b in user_agent for b in ['Mozilla', 'Chrome', 'Safari', 'Edge']): - abort(403) - - # 3. JSON body validation. Many API handlers safely default missing JSON to - # an empty payload, but malformed JSON should fail before route logic runs. - if request.method in ['POST', 'PUT', 'DELETE', 'PATCH'] and request.is_json: - try: - request.get_json(silent=False) - except BadRequest: - return jsonify({ - "success": False, - "error": "Invalid JSON payload", - }), 400 - -# ─── Routes ─────────────────────────────────────────────────────── + user_agent = request.headers.get('User-Agent', '') + if any(b in user_agent for b in ['Mozilla', 'Chrome', 'Safari', 'Edge']): + abort(403) + + # 3. JSON body validation. Many API handlers safely default missing JSON to + # an empty payload, but malformed JSON should fail before route logic runs. + if request.method in ['POST', 'PUT', 'DELETE', 'PATCH'] and request.is_json: + try: + request.get_json(silent=False) + except BadRequest: + return jsonify({ + "success": False, + "error": "Invalid JSON payload", + }), 400 + +# ─── Routes ─────────────────────────────────────────────────────── @app.route("/") diff --git a/ui/app.js b/ui/app.js index bf7725b..f75230b 100644 --- a/ui/app.js +++ b/ui/app.js @@ -35,6 +35,7 @@ const API = { let state = { scripts: {}, activeScript: null, + expandedCategories: new Set(['Favorites']), lockTarget: null, expandedCategories: new Set(), expandedRoot: true, @@ -79,6 +80,7 @@ let state = { reliabilityFilter: 'all', reliabilitySearch: '', reliabilityDiagnostics: null, + terminalStatuses: {}, }; const unlockCredentials = new Map(); @@ -87,6 +89,96 @@ function isScriptUnlocked(relPath) { return !!state.unlockedScripts[relPath]; } +function getTerminalDisplayName(termId) { + termId = Number(termId); + const index = state.terminals.indexOf(termId); + return index !== -1 ? `Terminal ${index + 1}` : `Terminal ${termId}`; +} + +function renumberTabs() { + state.terminals.forEach((id, index) => { + const tabBtn = document.getElementById(`tab-btn-${id}`) || document.querySelector(`.cli-tab[data-id="${id}"]`); + if (tabBtn) { + const spans = tabBtn.getElementsByTagName('span'); + for (let span of spans) { + if (span.textContent.trim().startsWith('Terminal')) { + span.textContent = getTerminalDisplayName(id); + break; + } + } + } + }); +} + +function updateTabStatusIndicator(termId) { + termId = Number(termId); + const indicator = document.getElementById(`tab-status-${termId}`); + if (!indicator) return; + + const status = state.terminalStatuses[termId]; + if (status === 'running') { + indicator.innerHTML = ``; + } else if (status === 'success') { + indicator.innerHTML = ``; + } else if (status === 'failed') { + indicator.innerHTML = ``; + } else { + indicator.innerHTML = ''; + } +} + +function showCustomConfirm(message, title = 'Confirmation') { + return new Promise((resolve) => { + const overlay = document.getElementById('confirm-modal-overlay'); + const titleEl = document.getElementById('confirm-modal-title'); + const messageEl = document.getElementById('confirm-modal-message'); + const confirmBtn = document.getElementById('confirm-modal-confirm'); + const cancelBtn = document.getElementById('confirm-modal-cancel'); + const closeBtn = document.getElementById('confirm-modal-close'); + + if (!overlay || !titleEl || !messageEl || !confirmBtn || !cancelBtn || !closeBtn) { + resolve(confirm(message)); + return; + } + + titleEl.textContent = title; + messageEl.textContent = message; + + overlay.classList.add('active'); + confirmBtn.focus(); + + const cleanup = (value) => { + overlay.classList.remove('active'); + confirmBtn.removeEventListener('click', onConfirm); + cancelBtn.removeEventListener('click', onCancel); + closeBtn.removeEventListener('click', onCancel); + document.removeEventListener('keydown', onKeyDown); + resolve(value); + }; + + const onConfirm = () => cleanup(true); + const onCancel = () => cleanup(false); + const onKeyDown = (e) => { + if (e.key === 'Escape') { + onCancel(); + } + }; + + confirmBtn.addEventListener('click', onConfirm); + cancelBtn.addEventListener('click', onCancel); + closeBtn.addEventListener('click', onCancel); + document.addEventListener('keydown', onKeyDown); + }); +} + +function changeTerminalFontSize(delta) { + let terminalFontSize = parseInt(localStorage.getItem('terminal-font-size')) || 13; + terminalFontSize = Math.max(9, Math.min(24, terminalFontSize + delta)); + localStorage.setItem('terminal-font-size', terminalFontSize); + document.documentElement.style.setProperty('--terminal-font-size', `${terminalFontSize}px`); +} + + function getUnlockPassword(relPath) { return unlockCredentials.get(relPath) || ''; } @@ -177,6 +269,77 @@ if (!window.__devshell_lifecycle_registered) { } // ─── Init ────────────────────────────────────────────────── +const ANALYTICS_TEMPLATE = ` +
+
+
+

Total Runs

+ +
+
0
+
+
+
+
All executions
+
+
+
+

Successful

+ +
+
0
+
+
+
+
0% rate
+
+
+
+

Failed

+ +
+
0
+
+
+
+
0% rate
+
+
+
+

Average Runtime

+ +
+
0s
+
+
+
+
Latency average
+
+
+
+
+

Top Executed Scripts

+
+
+
+

Slowest Executions

+
+
+
+

Recent Failures

+
+
+
+`; + +const ANALYTICS_EMPTY_TEMPLATE = ` +
+ +

No Execution History

+

Analytics will appear here after you run commands or execute scripts.

+
+`; + async function openAnalytics() { try { const res = await fetch('/api/history/analytics'); @@ -188,32 +351,60 @@ async function openAnalytics() { } const summary = data.summary; + const total = summary.total || 0; + + const contentEl = document.getElementById('analytics-content'); + if (!contentEl) return; - document.getElementById('analytics-total').textContent = summary.total; + if (total === 0) { + contentEl.innerHTML = ANALYTICS_EMPTY_TEMPLATE; + document.getElementById('analytics-modal-overlay').classList.add('active'); + return; + } + + contentEl.innerHTML = ANALYTICS_TEMPLATE; + + const successRate = total > 0 ? Math.round((summary.successful / total) * 100) : 0; + const failedRate = total > 0 ? Math.round((summary.failed / total) * 100) : 0; + + document.getElementById('analytics-total').textContent = total; document.getElementById('analytics-success').textContent = summary.successful; document.getElementById('analytics-failed').textContent = summary.failed; document.getElementById('analytics-avg').textContent = `${summary.avg_duration}s`; + const successFill = document.getElementById('analytics-success-fill'); + const successRateEl = document.getElementById('analytics-success-rate'); + if (successFill) successFill.style.width = `${successRate}%`; + if (successRateEl) successRateEl.textContent = `${successRate}% rate`; + + const failedFill = document.getElementById('analytics-failed-fill'); + const failedRateEl = document.getElementById('analytics-failed-rate'); + if (failedFill) failedFill.style.width = `${failedRate}%`; + if (failedRateEl) failedRateEl.textContent = `${failedRate}% rate`; + document.getElementById('analytics-top-scripts').innerHTML = data.top_scripts.map(([name, count]) => `
- ${escapeHtml(name)} — ${count} runs + ${escapeHtml(name)} + ${count} ${count === 1 ? 'run' : 'runs'}
- `).join(''); + `).join('') || '
No execution data available.
'; document.getElementById('analytics-slowest').innerHTML = data.slowest.map(entry => `
- ${escapeHtml(entry.display_name)} — ${entry.duration_seconds}s + ${escapeHtml(entry.display_name)} + ${entry.duration_seconds}s
- `).join(''); + `).join('') || '
No slow executions recorded.
'; document.getElementById('analytics-failures').innerHTML = data.recent_failures.map(entry => `
- ${escapeHtml(entry.display_name)} — Exit ${entry.exit_code} + ${escapeHtml(entry.display_name)} + Exit ${entry.exit_code}
- `).join(''); + `).join('') || '
No recent failures recorded.
'; document.getElementById('analytics-modal-overlay').classList.add('active'); @@ -884,6 +1075,9 @@ async function loadCommandHistory() { } document.addEventListener('DOMContentLoaded', async () => { + let terminalFontSize = parseInt(localStorage.getItem('terminal-font-size')) || 13; + document.documentElement.style.setProperty('--terminal-font-size', `${terminalFontSize}px`); + await loadScripts(); await loadCommandHistory(); bindEvents(); @@ -994,6 +1188,7 @@ async function fetchScriptContent(relPath, password = '') { } function getTerminalBody(termId = state.activeTerminalId) { + termId = Number(termId); return document.getElementById(`terminal-body-${termId}`) || (termId === 1 ? document.getElementById('terminal-body') : null); } @@ -1148,6 +1343,8 @@ async function executeScriptWithArguments(relPath, argumentsText) { controller: controller }; updateRunButton(); + state.terminalStatuses[termId] = 'running'; + updateTabStatusIndicator(termId); if (termId === state.activeTerminalId) { runStatus.textContent = 'Executing...'; @@ -1252,6 +1449,8 @@ async function executeScriptWithArguments(relPath, argumentsText) { } } else if (data.type === 'aborted') { receivedTerminalEvent = true; + state.terminalStatuses[termId] = 'failed'; + updateTabStatusIndicator(termId); appendToCli(data.content, 'error', termId); if (typeof DebuggerConsole !== 'undefined') { DebuggerConsole.addEntry('error', `Script aborted (ID: ${data.run_id})`, 'script'); @@ -1263,6 +1462,8 @@ async function executeScriptWithArguments(relPath, argumentsText) { } else if (data.type === 'metrics') { receivedTerminalEvent = true; if (data.success) { + state.terminalStatuses[termId] = 'success'; + updateTabStatusIndicator(termId); appendToCli(`Script completed (Exit code: ${data.exit_code})`, 'success', termId); if (typeof DebuggerConsole !== 'undefined') { DebuggerConsole.addEntry('info', `✓ Script completed — exit code: ${data.exit_code} | time: ${data.resources?.execution_time_formatted || ''} | cpu: ${data.resources?.cpu_percent || 0}% | mem: ${data.resources?.memory_used_mb || 0}MB`, 'metrics'); @@ -1285,6 +1486,8 @@ async function executeScriptWithArguments(relPath, argumentsText) { }, 3000); } } else { + state.terminalStatuses[termId] = 'failed'; + updateTabStatusIndicator(termId); appendToCli(`Script failed (Exit code: ${data.exit_code})`, 'error', termId); if (typeof DebuggerConsole !== 'undefined') { DebuggerConsole.addEntry('error', `✗ Script failed — exit code: ${data.exit_code}`, 'metrics'); @@ -1324,6 +1527,8 @@ async function executeScriptWithArguments(relPath, argumentsText) { const running = state.runningScripts[termId]; if (!receivedTerminalEvent && running && !running.abortRequested) { + state.terminalStatuses[termId] = 'failed'; + updateTabStatusIndicator(termId); appendToCli('Connection to script stream lost unexpectedly.', 'error', termId); if (termId === state.activeTerminalId) { runStatus.textContent = 'Disconnected'; @@ -1338,6 +1543,8 @@ async function executeScriptWithArguments(relPath, argumentsText) { } } } catch (err) { + state.terminalStatuses[termId] = 'failed'; + updateTabStatusIndicator(termId); if (err.name === 'AbortError') { appendToCli('Script run aborted.', 'system', termId); if (termId === state.activeTerminalId) { @@ -1675,7 +1882,7 @@ async function saveScript(category, filename, content) { } async function deleteScript(relPath) { - if (!confirm('Are you sure you want to delete this script permanently?')) return; + if (!await showCustomConfirm('Are you sure you want to delete this script permanently?', 'Delete Script')) return; try { const res = await fetch(API.delete, { method: 'DELETE', @@ -1904,8 +2111,9 @@ async function executePR(relPath, branch, message, repoUrl) { // Offer PR page opening if ( - confirm( - `Successfully pushed to branch '${data.branch}'.\n\nWould you like to open the Pull Request page on GitHub?` + await showCustomConfirm( + `Successfully pushed to branch '${data.branch}'.\n\nWould you like to open the Pull Request page on GitHub?`, + 'PR Created' ) ) { window.open(data.pr_url, '_blank', 'noopener,noreferrer'); @@ -2177,7 +2385,7 @@ function appendToCli(text, className = '', termId = state.activeTerminalId) { function clearCli() { const termBody = getTerminalBody(state.activeTerminalId); if (termBody) { - termBody.innerHTML = '
$ Terminal cleared.
'; + termBody.innerHTML = '
$Terminal cleared.
Ready for new commands.
'; } document.getElementById('run-status').textContent = ''; document.getElementById('run-status').className = 'run-status'; @@ -2190,207 +2398,7 @@ function clearCli() { } -// ─── Session Persistence ────────────────────────────────── - -async function saveSession() { - const sessionData = { - sessionId: state.sessionId || generateUUID(), - timestamp: Date.now(), - - terminals: state.terminals.map(id => { - const body = - document.getElementById(`terminal-body-${id}`) || - (id === 1 - ? document.getElementById('terminal-body') - : null); - - if (!body) return null; - - const lines = Array.from( - body.querySelectorAll('.cli-output-block') - ) - .slice(-100) - .map(el => ({ - text: el.textContent, - className: el.className.replace( - 'cli-output-block ', - '' - ) - })); - - return { - id, - lines - }; - }).filter(t => t !== null), - - activeTerminalId: state.activeTerminalId, - nextTerminalId: state.nextTerminalId, - - cmdHistory: state.cmdHistory, - cmdHistoryIndex: state.cmdHistoryIndex, - - unlockedScripts: serializeUnlockedScripts() - }; - - try { - await fetch('/api/sessions/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - session: sessionData - }) - }); - - state.sessionId = sessionData.sessionId; - state.lastSaveTimestamp = Date.now(); - - } catch (e) { - console.error('Failed to save session:', e); - } -} - - -function generateUUID() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' - .replace(/[xy]/g, c => { - const r = Math.random() * 16 | 0; - const v = c === 'x' - ? r - : (r & 0x3 | 0x8); - - return v.toString(16); - }); -} - - -// let saveSessionTimeout = null; - -function saveSessionDebounced() { - if (saveSessionTimeout) { - clearTimeout(saveSessionTimeout); - } - - saveSessionTimeout = setTimeout(() => { - saveSession(); - }, 2000); -} - - -async function restoreSession() { - try { - const res = await fetch('/api/sessions/restore'); - const data = await res.json(); - - if (!data.success || !data.session) { - return; - } - - const session = data.session; - - state.sessionId = session.sessionId || null; - - const terminalIds = session.terminals?.map(t => t.id); - state.terminals = terminalIds?.length ? terminalIds : [1]; - - state.activeTerminalId = - session.activeTerminalId || 1; - - state.nextTerminalId = - Math.max(...state.terminals) + 1; - - state.cmdHistory = - session.cmdHistory || []; - - state.cmdHistoryIndex = - session.cmdHistoryIndex || -1; - - restoreUnlockedScripts(session.unlockedScripts); - - const existingTabs = - document.querySelectorAll('.cli-tab'); - - existingTabs.forEach(tab => { - if (tab.id !== 'tab-btn-1') { - tab.remove(); - } - }); - const existingBodies = - document.querySelectorAll('.cli-body'); - - existingBodies.forEach(body => { - if (body.id !== 'terminal-body') { - body.remove(); - } - }); - - for (const term of session.terminals || []) { - - if (term.id !== 1) { - // Create terminal DOM directly with the saved ID - // instead of calling addTerminal() which would - // corrupt state.nextTerminalId and state.terminals - const tabsContainer = document.getElementById('cli-tabs'); - const tabBtn = document.createElement('div'); - tabBtn.className = 'cli-tab'; - tabBtn.id = `tab-btn-${term.id}`; - tabBtn.innerHTML = ` - - - - - - Terminal ${term.id} - `; - tabBtn.onclick = () => switchTerminal(term.id); - tabsContainer.insertBefore(tabBtn, document.getElementById('btn-add-tab')); - - const bodyContainer = document.createElement('div'); - bodyContainer.className = 'cli-body'; - bodyContainer.setAttribute('role', 'log'); - bodyContainer.setAttribute('aria-live', 'polite'); - bodyContainer.id = `terminal-body-${term.id}`; - bodyContainer.style.display = 'none'; - - document.getElementById('cli-area').insertBefore( - bodyContainer, - document.querySelector('.cli-input-bar') - ); - } - - const body = - document.getElementById(`terminal-body-${term.id}`) || - (term.id === 1 - ? document.getElementById('terminal-body') - : null); - - if (!body) continue; - - body.innerHTML = ''; - - for (const line of term.lines || []) { - const div = document.createElement('div'); - - div.className = - `cli-output-block ${line.className}`; - - div.textContent = line.text; - - body.appendChild(div); - } - } - - switchTerminal(state.activeTerminalId); - - console.log('Session restored successfully'); - - } catch (e) { - console.error('Failed to restore session:', e); - } -} // ─── Terminal Utility Actions ─────────────────────────────── @@ -2478,7 +2486,7 @@ function toggleAutoScroll() { state.autoScroll[termId] = state.autoScroll[termId] === false ? true : false; const isOn = state.autoScroll[termId] !== false; updateAutoScrollBtn(termId, isOn); - notify(`Auto-scroll ${isOn ? 'enabled' : 'disabled'} for Terminal ${termId}.`, 'info'); + notify(`Auto-scroll ${isOn ? 'enabled' : 'disabled'} for ${getTerminalDisplayName(termId)}.`, 'info'); } /** @@ -2533,7 +2541,7 @@ function setTerminalDensity(density) { function clearCli() { const termBody = getTerminalBody(state.activeTerminalId); if (termBody) { - termBody.innerHTML = '
$ Terminal cleared.
'; + termBody.innerHTML = '
$Terminal cleared.
Ready for new commands.
'; } document.getElementById('run-status').textContent = ''; document.getElementById('run-status').className = 'run-status'; @@ -2553,11 +2561,8 @@ async function saveSession() { timestamp: Date.now(), terminals: state.terminals.map(id => { - const body = - document.getElementById(`terminal-body-${id}`) || - (id === 1 - ? document.getElementById('terminal-body') - : null); + id = Number(id); + const body = getTerminalBody(id); if (!body) return null; @@ -2579,8 +2584,8 @@ async function saveSession() { }; }).filter(t => t !== null), - activeTerminalId: state.activeTerminalId, - nextTerminalId: state.nextTerminalId, + activeTerminalId: Number(state.activeTerminalId), + nextTerminalId: Number(state.nextTerminalId), cmdHistory: state.cmdHistory, cmdHistoryIndex: state.cmdHistoryIndex, @@ -2647,97 +2652,77 @@ async function restoreSession() { state.sessionId = session.sessionId || null; - const terminalIds = session.terminals?.map(t => t.id); + const terminalIds = session.terminals?.map(t => Number(t.id)); state.terminals = terminalIds?.length ? terminalIds : [1]; - state.activeTerminalId = - session.activeTerminalId || 1; - - state.nextTerminalId = - Math.max(...state.terminals) + 1; + state.activeTerminalId = Number(session.activeTerminalId || 1); - state.cmdHistory = - session.cmdHistory || []; + state.nextTerminalId = Math.max(...state.terminals, 1) + 1; - state.cmdHistoryIndex = - session.cmdHistoryIndex || -1; + state.cmdHistory = session.cmdHistory || []; + state.cmdHistoryIndex = session.cmdHistoryIndex || -1; restoreUnlockedScripts(session.unlockedScripts); - const existingTabs = - document.querySelectorAll('.cli-tab'); - - existingTabs.forEach(tab => { - if (tab.id !== 'tab-btn-1') { - tab.remove(); - } + // Remove all existing terminal tabs from the DOM + document.querySelectorAll('.cli-tab').forEach(tab => { + tab.remove(); }); - const existingBodies = - document.querySelectorAll('.cli-body'); - - existingBodies.forEach(body => { - if (body.id !== 'terminal-body') { - body.remove(); - } + // Remove all existing terminal bodies from the DOM + document.querySelectorAll('.cli-body').forEach(body => { + body.remove(); }); - for (const term of session.terminals || []) { - - if (term.id !== 1) { - // Create terminal DOM directly with the saved ID - // instead of calling addTerminal() which would - // corrupt state.nextTerminalId and state.terminals - const tabsContainer = document.getElementById('cli-tabs'); - const tabBtn = document.createElement('div'); - tabBtn.className = 'cli-tab'; - tabBtn.id = `tab-btn-${term.id}`; - tabBtn.innerHTML = ` - - - - - - Terminal ${term.id} - `; - tabBtn.onclick = () => switchTerminal(term.id); - tabsContainer.insertBefore(tabBtn, document.getElementById('btn-add-tab')); - - const bodyContainer = document.createElement('div'); - bodyContainer.className = 'cli-body'; - bodyContainer.setAttribute('role', 'log'); - bodyContainer.setAttribute('aria-live', 'polite'); - bodyContainer.id = `terminal-body-${term.id}`; - bodyContainer.style.display = 'none'; - - document.getElementById('cli-area').insertBefore( - bodyContainer, - document.querySelector('.cli-input-bar') - ); - } - - const body = - document.getElementById(`terminal-body-${term.id}`) || - (term.id === 1 - ? document.getElementById('terminal-body') - : null); - - if (!body) continue; - - body.innerHTML = ''; - - for (const line of term.lines || []) { - const div = document.createElement('div'); + const tabsContainer = document.getElementById('cli-tabs'); + const cliArea = document.getElementById('cli-area'); - div.className = - `cli-output-block ${line.className}`; - - div.textContent = line.text; + for (const term of session.terminals || []) { + const termId = Number(term.id); + + const tabBtn = document.createElement('div'); + tabBtn.className = 'cli-tab'; + tabBtn.id = `tab-btn-${termId}`; + tabBtn.dataset.id = termId; + tabBtn.innerHTML = ` + >_ + ${getTerminalDisplayName(termId)} + `; + tabBtn.onclick = () => switchTerminal(termId); + tabsContainer.insertBefore(tabBtn, document.getElementById('btn-add-tab')); + + const bodyContainer = document.createElement('div'); + bodyContainer.className = 'cli-body'; + bodyContainer.setAttribute('role', 'log'); + bodyContainer.setAttribute('aria-live', 'polite'); + bodyContainer.id = `terminal-body-${termId}`; + bodyContainer.style.display = 'none'; + + cliArea.insertBefore( + bodyContainer, + document.querySelector('.cli-input-bar') + ); - body.appendChild(div); + // Populate terminal log lines + bodyContainer.innerHTML = ''; + if (!term.lines || term.lines.length === 0) { + if (termId === 1) { + bodyContainer.innerHTML = '
$Welcome to DevShell.
Select a script to run, or type a command below.
'; + } else { + bodyContainer.innerHTML = '
$Terminal ready.
Ready for interaction.
'; + } + } else { + for (const line of term.lines || []) { + const div = document.createElement('div'); + div.className = `cli-output-block ${line.className}`; + div.textContent = line.text; + bodyContainer.appendChild(div); + } } } + renumberTabs(); + state.terminals.forEach(termId => updateTabStatusIndicator(termId)); switchTerminal(state.activeTerminalId); console.log('Session restored successfully'); @@ -2759,15 +2744,12 @@ function addTerminal() { tabBtn.className = 'cli-tab'; tabBtn.id = `tab-btn-${id}`; tabBtn.innerHTML = ` - - - - - - Terminal ${id} + >_ + ${getTerminalDisplayName(id)} `; tabBtn.onclick = () => switchTerminal(id); tabsContainer.insertBefore(tabBtn, document.getElementById('btn-add-tab')); + renumberTabs(); const bodyContainer = document.createElement('div'); bodyContainer.className = 'cli-body'; @@ -2775,7 +2757,7 @@ function addTerminal() { bodyContainer.setAttribute('aria-live', 'polite'); bodyContainer.id = `terminal-body-${id}`; bodyContainer.style.display = 'none'; - bodyContainer.innerHTML = '
$ Terminal ready.
'; + bodyContainer.innerHTML = '
$Terminal ready.
Ready for interaction.
'; document.getElementById('cli-area').insertBefore(bodyContainer, document.querySelector('.cli-input-bar')); applyTerminalDensity(); @@ -2785,6 +2767,7 @@ function addTerminal() { } function switchTerminal(id) { + id = Number(id); state.activeTerminalId = id; document.querySelectorAll('.cli-tab').forEach(t => t.classList.remove('active')); @@ -2818,6 +2801,7 @@ function switchTerminal(id) { } function closeTerminal(id) { + id = Number(id); if (state.terminals.length <= 1) return; if (state.runningScripts && state.runningScripts[id]) { @@ -2826,9 +2810,11 @@ function closeTerminal(id) { state.terminals = state.terminals.filter(t => t !== id); delete state.autoScroll[id]; + delete state.terminalStatuses[id]; const tabBtn = document.getElementById(`tab-btn-${id}`) || document.querySelector(`.cli-tab[data-id="${id}"]`); if (tabBtn) tabBtn.remove(); + renumberTabs(); const bodyContainer = getTerminalBody(id); if (bodyContainer) bodyContainer.remove(); @@ -2911,7 +2897,8 @@ function renderSidebar() { const tree = document.getElementById('category-tree'); const countEl = document.getElementById('script-count'); const favsSection = document.getElementById('favorites-section'); - const favsList = document.getElementById('favorites-list'); + + if (favsSection) favsSection.style.display = 'none'; let totalScripts = 0; let favScripts = []; @@ -2919,6 +2906,63 @@ function renderSidebar() { const query = state.searchQuery.toLowerCase(); + // Collect all favorites matching search query + for (const [cat, scripts] of Object.entries(state.scripts)) { + scripts.forEach(s => { + if (s.favorite) { + const matches = !query || + s.name.toLowerCase().includes(query) || + (s.desc && s.desc.toLowerCase().includes(query)) || + (s.tag && s.tag.toLowerCase().includes(query)) || + s.file.toLowerCase().includes(query); + if (matches) { + favScripts.push(s); + } + } + }); + } + + // Render Favorites virtual category folder + if (favScripts.length > 0) { + const isExpanded = state.expandedCategories.has('Favorites') || !!query; + html += ` +
+
+ + + + ${ICONS.favorite} + Favorites + ${favScripts.length} +
+ +
+ `; + } + + // Render other categories for (const [cat, scripts] of Object.entries(state.scripts)) { const filteredScripts = query ? scripts.filter(s => @@ -2946,8 +2990,7 @@ function renderSidebar() {