feat (ui): exec fase 3

This commit is contained in:
kodi
2026-03-06 18:13:55 +01:00
parent 39a33e5711
commit a4099867a5
3 changed files with 121 additions and 6 deletions
+12
View File
@@ -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;
+104 -5
View File
@@ -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;