From a4099867a59175a28efbcf952d70df1a9e15f8a5 Mon Sep 17 00:00:00 2001 From: kodi Date: Fri, 6 Mar 2026 18:13:55 +0100 Subject: [PATCH] feat (ui): exec fase 3 --- webui/html/assets/css/app.css | 12 +++ webui/html/assets/js/tabs/containers.js | 109 ++++++++++++++++++++++-- webui/html/index.html | 6 +- 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/webui/html/assets/css/app.css b/webui/html/assets/css/app.css index 15a48a6..bb5941e 100644 --- a/webui/html/assets/css/app.css +++ b/webui/html/assets/css/app.css @@ -467,6 +467,14 @@ pre{ flex-direction: column; gap: 10px; } +.execTerminalHost{ + min-height: 48vh; + max-height: 64vh; + border: 1px solid var(--soft-line); + border-radius: 12px; + background: #0a0f1d; + overflow: hidden; +} .execOutput{ margin: 0; white-space: pre-wrap; @@ -482,6 +490,10 @@ pre{ max-height: 64vh; overflow: auto; } +.execTerminalHost .xterm, +.execTerminalHost .xterm-viewport{ + height: 100%; +} .execInputRow{ display: flex; align-items: center; diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js index bc9cf5b..9a44007 100644 --- a/webui/html/assets/js/tabs/containers.js +++ b/webui/html/assets/js/tabs/containers.js @@ -404,6 +404,9 @@ let execTerminalState = { container: '', sessionId: '', source: null, + term: null, + fitAddon: null, + useXterm: false, reconnectTimer: null, reconnectAttempts: 0, reconnectEnabled: false, @@ -433,6 +436,8 @@ function _execEls() { back: document.getElementById('execModalBack'), title: document.getElementById('execModalTitle'), output: document.getElementById('execTerminalOutput'), + termHost: document.getElementById('execTerminalHost'), + inputRow: document.getElementById('execInputRow'), bufferInfo: document.getElementById('execTerminalBufferInfo'), input: document.getElementById('execTerminalInput'), sendBtn: document.getElementById('execTerminalSendBtn'), @@ -447,12 +452,88 @@ function _execSetStatus(msg) { if (status) status.textContent = msg || ''; } +function _execCanUseXterm() { + return !!(window.Terminal && window.FitAddon && window.FitAddon.FitAddon); +} + +function _execSetTerminalUiMode(useXterm) { + const { termHost, output, inputRow, rawBtn } = _execEls(); + if (termHost) termHost.style.display = useXterm ? 'block' : 'none'; + if (output) output.style.display = useXterm ? 'none' : 'block'; + if (inputRow) inputRow.style.display = useXterm ? 'none' : 'flex'; + if (rawBtn) rawBtn.style.display = useXterm ? 'none' : 'inline-flex'; +} + +function _execDestroyTerminal() { + if (execTerminalState.term) { + try { execTerminalState.term.dispose(); } catch (_) {} + } + execTerminalState.term = null; + execTerminalState.fitAddon = null; + execTerminalState.useXterm = false; +} + +function _execInitTerminal() { + const { termHost } = _execEls(); + if (!_execCanUseXterm() || !termHost) { + _execDestroyTerminal(); + _execSetTerminalUiMode(false); + return false; + } + + _execDestroyTerminal(); + try { + const fitAddon = new window.FitAddon.FitAddon(); + const term = new window.Terminal({ + cursorBlink: true, + convertEol: false, + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontSize: 13, + scrollback: 5000, + theme: { + background: '#0a0f1d', + foreground: '#d9e6ff', + }, + }); + term.loadAddon(fitAddon); + term.open(termHost); + fitAddon.fit(); + + term.onData((data) => { + const payload = String(data || ''); + if (!payload) return; + const p = payload.length > EXEC_PASTE_CHUNK_SIZE + ? _execEnqueueChunkedInput(payload) + : _execEnqueueInput(payload); + p.catch((e) => { + _execAppendOutput(`\n[ERROR] ${e.message || e}\n`); + _execSwitchToLineFallback('xterm input error'); + }); + }); + + execTerminalState.term = term; + execTerminalState.fitAddon = fitAddon; + execTerminalState.useXterm = true; + _execSetTerminalUiMode(true); + term.focus(); + return true; + } catch (_) { + _execDestroyTerminal(); + _execSetTerminalUiMode(false); + return false; + } +} + function _execConnectedStatus() { const sid = execTerminalState.sessionId || ''; return sid ? `Connected (${sid})` : 'Connected'; } function _execAppendOutput(txt) { + if (execTerminalState.useXterm && execTerminalState.term) { + try { execTerminalState.term.write(String(txt || '')); } catch (_) {} + return; + } const { output } = _execEls(); if (!output || !txt) return; @@ -469,6 +550,9 @@ function _execAppendOutput(txt) { } function _execClearOutput() { + if (execTerminalState.useXterm && execTerminalState.term) { + try { execTerminalState.term.reset(); } catch (_) {} + } const { output } = _execEls(); execTerminalState.outputText = ''; execTerminalState.outputLineCount = 0; @@ -478,6 +562,7 @@ function _execClearOutput() { } function _execUpdateBufferInfo() { + if (execTerminalState.useXterm) return; const { bufferInfo } = _execEls(); if (!bufferInfo) return; if (execTerminalState.outputTruncated) { @@ -611,10 +696,11 @@ function _execAttachStream(afterSeq = 0, wasReconnect = false) { } function _execComputeSize() { - const { output } = _execEls(); - if (!output) return { rows: 24, cols: 80 }; - const w = output.clientWidth || 640; - const h = output.clientHeight || 360; + const { output, termHost } = _execEls(); + const target = (execTerminalState.useXterm && termHost) ? termHost : output; + if (!target) return { rows: 24, cols: 80 }; + const w = target.clientWidth || 640; + const h = target.clientHeight || 360; const cols = Math.max(20, Math.floor(w / 8)); const rows = Math.max(8, Math.floor(h / 18)); return { rows, cols }; @@ -623,6 +709,9 @@ function _execComputeSize() { async function _execResizeNow() { const sid = execTerminalState.sessionId; if (!sid) return; + if (execTerminalState.useXterm && execTerminalState.fitAddon) { + try { execTerminalState.fitAddon.fit(); } catch (_) {} + } const { rows, cols } = _execComputeSize(); try { await api(`/containers/exec/${encodeURIComponent(sid)}/resize`, 'POST', { rows, cols }); @@ -654,6 +743,8 @@ function _execUpdateInputModeUi() { } function _execSwitchToLineFallback(reason) { + _execDestroyTerminal(); + _execSetTerminalUiMode(false); if (!execTerminalState.rawMode) return; execTerminalState.rawMode = false; _execUpdateInputModeUi(); @@ -799,6 +890,7 @@ async function containerExecOpen(name) { execTerminalState.reconnectEnabled = false; _execCancelReconnect(); execTerminalState.rawMode = true; + _execInitTerminal(); _execUpdateInputModeUi(); _execResetInputQueue(); _execCloseEventSource(); @@ -829,7 +921,11 @@ async function containerExecOpen(name) { execTerminalState.resizeObserver.observe(els.modal); } - if (els.input) els.input.focus(); + if (execTerminalState.useXterm && execTerminalState.term) { + execTerminalState.term.focus(); + } else if (els.input) { + els.input.focus(); + } } catch (e) { _execSetStatus('Start failed'); _execAppendOutput(`\n[ERROR] ${e.message || e}\n`); @@ -890,6 +986,7 @@ function containerExecHandleInputBlur() { } function containerExecToggleRawMode() { + if (execTerminalState.useXterm) return; execTerminalState.rawMode = !execTerminalState.rawMode; _execUpdateInputModeUi(); _execFocusInputSoon(0); @@ -902,6 +999,8 @@ async function containerExecClose() { execTerminalState.reconnectEnabled = false; _execCancelReconnect(); _execCloseEventSource(); + _execDestroyTerminal(); + _execSetTerminalUiMode(false); if (execTerminalState.resizeObserver) { try { execTerminalState.resizeObserver.disconnect(); } catch (_) {} execTerminalState.resizeObserver = null; diff --git a/webui/html/index.html b/webui/html/index.html index bdfb63b..90c602b 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -8,6 +8,7 @@ + MVP Control UI @@ -371,8 +372,9 @@
+

-      
+
+ +