mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
Keep terminal card from covering transcript
This commit is contained in:
@@ -443,7 +443,6 @@
|
||||
.messages.queue-open{padding-bottom:var(--queue-card-height,280px);}
|
||||
/* Terminal flyout reserves transcript space so recent messages stay readable above it. */
|
||||
.messages.terminal-open{padding-bottom:var(--terminal-card-height,320px);scroll-padding-bottom:var(--terminal-card-height,320px);transition:padding-bottom .26s cubic-bezier(.2,.8,.2,1);}
|
||||
.messages.terminal-collapsed{padding-bottom:var(--terminal-dock-height,72px);scroll-padding-bottom:var(--terminal-dock-height,72px);transition:padding-bottom .22s cubic-bezier(.2,.8,.2,1);}
|
||||
.queue-card-inner{background:var(--surface);border:1px solid var(--border);border-bottom:none;border-radius:14px 14px 0 0;contain:paint;transform:translateY(100%);opacity:0;transition:transform .35s cubic-bezier(.32,.72,.16,1),opacity .2s ease;overflow:hidden;max-height:240px;overflow-y:auto;padding-bottom:4px;}
|
||||
.queue-card.visible .queue-card-inner{transform:translateY(0);opacity:1;}
|
||||
.queue-card-header{display:flex;align-items:center;gap:8px;padding:9px 14px 8px;border-bottom:1px solid var(--border);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);}
|
||||
|
||||
+9
-121
@@ -160,106 +160,6 @@ function _terminalDimensions(){
|
||||
return {rows:18,cols:80};
|
||||
}
|
||||
|
||||
function _terminalHeightBounds(){
|
||||
const mobile=window.matchMedia&&window.matchMedia('(max-width: 700px)').matches;
|
||||
const min=mobile?TERMINAL_MOBILE_HEIGHT_MIN:TERMINAL_HEIGHT_MIN;
|
||||
const maxByViewport=Math.floor(window.innerHeight*(mobile?0.44:0.5));
|
||||
const hardMax=mobile?TERMINAL_MOBILE_HEIGHT_MAX:TERMINAL_HEIGHT_MAX;
|
||||
return {
|
||||
min,
|
||||
max:Math.max(min,Math.min(hardMax,maxByViewport)),
|
||||
defaultHeight:mobile?TERMINAL_MOBILE_HEIGHT_DEFAULT:TERMINAL_HEIGHT_DEFAULT,
|
||||
};
|
||||
}
|
||||
|
||||
function _clampTerminalHeight(height){
|
||||
const bounds=_terminalHeightBounds();
|
||||
const n=Number(height);
|
||||
const fallback=TERMINAL_UI.height||bounds.defaultHeight;
|
||||
return Math.max(bounds.min,Math.min(bounds.max,Number.isFinite(n)?n:fallback));
|
||||
}
|
||||
|
||||
function _applyTerminalHeight(height){
|
||||
const {inner,handle}= _terminalEls();
|
||||
const next=_clampTerminalHeight(height);
|
||||
TERMINAL_UI.height=next;
|
||||
if(inner)inner.style.setProperty('--composer-terminal-height',next+'px');
|
||||
if(handle){
|
||||
const bounds=_terminalHeightBounds();
|
||||
handle.setAttribute('aria-valuemin',String(bounds.min));
|
||||
handle.setAttribute('aria-valuemax',String(bounds.max));
|
||||
handle.setAttribute('aria-valuenow',String(next));
|
||||
}
|
||||
if(TERMINAL_UI.open&&!TERMINAL_UI.collapsed){
|
||||
_fitTerminal();
|
||||
_syncTerminalTranscriptSpace(true);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function _resetTerminalHeightForViewport(){
|
||||
const bounds=_terminalHeightBounds();
|
||||
_applyTerminalHeight(TERMINAL_UI.height||bounds.defaultHeight);
|
||||
}
|
||||
|
||||
function _startTerminalHeightResize(ev){
|
||||
if(ev.pointerType==='touch')return;
|
||||
const {inner,handle}= _terminalEls();
|
||||
if(!inner||!handle)return;
|
||||
ev.preventDefault();
|
||||
TERMINAL_UI.resizing=true;
|
||||
TERMINAL_UI.resizeStartY=ev.clientY;
|
||||
TERMINAL_UI.resizeStartHeight=TERMINAL_UI.height||inner.getBoundingClientRect().height||_terminalHeightBounds().defaultHeight;
|
||||
inner.classList.add('is-resizing');
|
||||
try{handle.setPointerCapture(ev.pointerId);}catch(_){}
|
||||
}
|
||||
|
||||
function _moveTerminalHeightResize(ev){
|
||||
if(!TERMINAL_UI.resizing)return;
|
||||
ev.preventDefault();
|
||||
_applyTerminalHeight(TERMINAL_UI.resizeStartHeight+(TERMINAL_UI.resizeStartY-ev.clientY));
|
||||
}
|
||||
|
||||
function _endTerminalHeightResize(ev){
|
||||
if(!TERMINAL_UI.resizing)return;
|
||||
TERMINAL_UI.resizing=false;
|
||||
const {inner,handle}= _terminalEls();
|
||||
if(inner)inner.classList.remove('is-resizing');
|
||||
if(handle&&ev&&ev.pointerId!==undefined)try{handle.releasePointerCapture(ev.pointerId);}catch(_){}
|
||||
_fitTerminal();
|
||||
}
|
||||
|
||||
function _handleTerminalResizeKey(ev){
|
||||
let delta=0;
|
||||
if(ev.key==='ArrowUp')delta=16;
|
||||
else if(ev.key==='ArrowDown')delta=-16;
|
||||
else if(ev.key==='PageUp')delta=64;
|
||||
else if(ev.key==='PageDown')delta=-64;
|
||||
else if(ev.key==='Home'){
|
||||
ev.preventDefault();
|
||||
return _applyTerminalHeight(_terminalHeightBounds().min);
|
||||
}
|
||||
else if(ev.key==='End'){
|
||||
ev.preventDefault();
|
||||
return _applyTerminalHeight(_terminalHeightBounds().max);
|
||||
}
|
||||
else return;
|
||||
ev.preventDefault();
|
||||
_applyTerminalHeight((TERMINAL_UI.height||_terminalHeightBounds().defaultHeight)+delta);
|
||||
}
|
||||
|
||||
function _initTerminalResizeHandle(){
|
||||
if(TERMINAL_UI.resizeHandleReady)return;
|
||||
const {handle}= _terminalEls();
|
||||
if(!handle)return;
|
||||
TERMINAL_UI.resizeHandleReady=true;
|
||||
handle.addEventListener('pointerdown',_startTerminalHeightResize);
|
||||
handle.addEventListener('pointermove',_moveTerminalHeightResize);
|
||||
handle.addEventListener('pointerup',_endTerminalHeightResize);
|
||||
handle.addEventListener('pointercancel',_endTerminalHeightResize);
|
||||
handle.addEventListener('keydown',_handleTerminalResizeKey);
|
||||
}
|
||||
|
||||
function _terminalMessagesEl(){
|
||||
return document.getElementById('messages');
|
||||
}
|
||||
@@ -269,38 +169,24 @@ function _terminalIsMessagesNearBottom(el){
|
||||
return el.scrollHeight-el.scrollTop-el.clientHeight<150;
|
||||
}
|
||||
|
||||
function _syncTerminalTranscriptSpace(open,opts){
|
||||
opts=opts||{};
|
||||
function _syncTerminalTranscriptSpace(open){
|
||||
const messages=_terminalMessagesEl();
|
||||
if(!messages)return;
|
||||
const wasNearBottom=_terminalIsMessagesNearBottom(messages);
|
||||
if(!open){
|
||||
messages.classList.remove('terminal-open');
|
||||
messages.classList.remove('terminal-collapsed');
|
||||
messages.style.removeProperty('--terminal-card-height');
|
||||
messages.style.removeProperty('--terminal-dock-height');
|
||||
if(wasNearBottom&&typeof scrollToBottom==='function')requestAnimationFrame(scrollToBottom);
|
||||
return;
|
||||
}
|
||||
if(open==='collapsed'){
|
||||
messages.classList.remove('terminal-open');
|
||||
messages.classList.add('terminal-collapsed');
|
||||
}else{
|
||||
messages.classList.add('terminal-open');
|
||||
messages.classList.remove('terminal-collapsed');
|
||||
}
|
||||
messages.classList.add('terminal-open');
|
||||
const measure=()=>{
|
||||
if(!TERMINAL_UI.open)return;
|
||||
const {panel,inner,dock}= _terminalEls();
|
||||
const target=open==='collapsed'?(dock||panel):(inner||panel);
|
||||
const h=target&&target.getBoundingClientRect().height;
|
||||
if(h>0){
|
||||
if(open==='collapsed')messages.style.setProperty('--terminal-dock-height',Math.ceil(h+24)+'px');
|
||||
else messages.style.setProperty('--terminal-card-height',Math.ceil(h+24)+'px');
|
||||
}
|
||||
const {panel,inner}= _terminalEls();
|
||||
const h=(inner||panel)&&((inner||panel).getBoundingClientRect().height);
|
||||
if(h>0)messages.style.setProperty('--terminal-card-height',Math.ceil(h+24)+'px');
|
||||
if(wasNearBottom&&typeof scrollToBottom==='function')scrollToBottom();
|
||||
};
|
||||
if(opts.immediate)measure();
|
||||
requestAnimationFrame(measure);
|
||||
setTimeout(measure,420);
|
||||
}
|
||||
@@ -312,6 +198,7 @@ function _fitTerminal(){
|
||||
try{
|
||||
if(TERMINAL_UI.fitAddon)TERMINAL_UI.fitAddon.fit();
|
||||
}catch(_){}
|
||||
_syncTerminalTranscriptSpace(true);
|
||||
_scheduleTerminalResize();
|
||||
}
|
||||
|
||||
@@ -428,8 +315,8 @@ async function toggleComposerTerminal(force){
|
||||
window.setTimeout(_fitTerminal,80);
|
||||
});
|
||||
TERMINAL_UI.open=true;
|
||||
TERMINAL_UI.collapsed=false;
|
||||
_syncTerminalTranscriptSpace(true);
|
||||
if(workspace)workspace.textContent=_terminalWorkspaceName();
|
||||
syncTerminalButton();
|
||||
if(!TERMINAL_UI.resizeObserver&&window.ResizeObserver){
|
||||
TERMINAL_UI.resizeObserver=new ResizeObserver(()=>_fitTerminal());
|
||||
@@ -491,7 +378,7 @@ async function closeComposerTerminal(sessionId,opts){
|
||||
}
|
||||
const {panel}= _terminalEls();
|
||||
if(panel){
|
||||
panel.classList.remove('is-open','is-collapsed');
|
||||
panel.classList.remove('is-open');
|
||||
_syncTerminalTranscriptSpace(false);
|
||||
clearTimeout(TERMINAL_UI.closeTimer);
|
||||
TERMINAL_UI.closeTimer=setTimeout(()=>{
|
||||
@@ -499,6 +386,7 @@ async function closeComposerTerminal(sessionId,opts){
|
||||
_disposeXterm();
|
||||
},280);
|
||||
}else{
|
||||
_syncTerminalTranscriptSpace(false);
|
||||
_disposeXterm();
|
||||
}
|
||||
TERMINAL_UI.open=false;
|
||||
|
||||
@@ -40,92 +40,20 @@ def test_terminal_surface_uses_composer_flyout_card_pattern():
|
||||
assert "transform:translateY(100%)" in style_css
|
||||
|
||||
|
||||
def test_terminal_uses_controlled_desktop_resize_handle():
|
||||
html = _read("static/index.html")
|
||||
style_css = _read("static/style.css")
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
assert 'class="composer-terminal-resize-handle"' in html
|
||||
assert 'role="separator"' in html
|
||||
assert 'aria-orientation="horizontal"' in html
|
||||
terminal_inner_rule = style_css.split(".composer-terminal-inner{", 1)[1].split("}", 1)[0]
|
||||
assert "resize:" not in terminal_inner_rule
|
||||
assert "cursor:ns-resize" in style_css
|
||||
assert "const TERMINAL_HEIGHT_DEFAULT=260" in terminal_js
|
||||
assert "const TERMINAL_HEIGHT_MIN=180" in terminal_js
|
||||
assert "const TERMINAL_HEIGHT_MAX=520" in terminal_js
|
||||
assert "max:Math.max(min,Math.min(hardMax,maxByViewport))" in terminal_js
|
||||
|
||||
|
||||
def test_terminal_resize_path_refits_backend_and_transcript_space():
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
assert "function _applyTerminalHeight" in terminal_js
|
||||
apply_block = terminal_js.split("function _applyTerminalHeight", 1)[1].split("function _resetTerminalHeightForViewport", 1)[0]
|
||||
assert "_fitTerminal();" in apply_block
|
||||
assert "_syncTerminalTranscriptSpace(true);" in apply_block
|
||||
assert "function _moveTerminalHeightResize" in terminal_js
|
||||
assert "_applyTerminalHeight(TERMINAL_UI.resizeStartHeight+(TERMINAL_UI.resizeStartY-ev.clientY))" in terminal_js
|
||||
assert "handle.addEventListener('pointerdown',_startTerminalHeightResize)" in terminal_js
|
||||
assert "handle.addEventListener('pointermove',_moveTerminalHeightResize)" in terminal_js
|
||||
assert "clearTimeout(TERMINAL_UI.resizeTimer)" in terminal_js
|
||||
assert "api('/api/terminal/resize'" in terminal_js
|
||||
|
||||
|
||||
def test_terminal_open_reserves_transcript_space():
|
||||
style_css = _read("static/style.css")
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
assert ".messages.terminal-open{padding-bottom:var(--terminal-card-height" in style_css
|
||||
assert ".messages.terminal-collapsed{padding-bottom:var(--terminal-dock-height" in style_css
|
||||
assert "scroll-padding-bottom:var(--terminal-card-height" in style_css
|
||||
assert "classList.add('terminal-open')" in terminal_js
|
||||
assert "classList.add('terminal-collapsed')" in terminal_js
|
||||
assert "classList.remove('terminal-open')" in terminal_js
|
||||
assert "classList.remove('terminal-collapsed')" in terminal_js
|
||||
assert "messages.style.setProperty('--terminal-card-height'" in terminal_js
|
||||
assert "messages.style.setProperty('--terminal-dock-height'" in terminal_js
|
||||
assert "messages.style.removeProperty('--terminal-card-height')" in terminal_js
|
||||
assert "messages.style.removeProperty('--terminal-dock-height')" in terminal_js
|
||||
assert "function _terminalIsMessagesNearBottom" in terminal_js
|
||||
assert "scrollToBottom" in terminal_js
|
||||
|
||||
|
||||
def test_terminal_collapsed_state_preserves_pty_and_output_surface():
|
||||
html = _read("static/index.html")
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
assert 'id="btnTerminalCollapse"' in html
|
||||
assert 'onclick="collapseComposerTerminal()"' in html
|
||||
assert 'id="btnTerminalExpand"' in html
|
||||
assert 'onclick="expandComposerTerminal()"' in html
|
||||
assert 'id="btnTerminalDockClose"' in html
|
||||
assert 'onclick="closeComposerTerminal()"' in html
|
||||
assert "collapsed:false" in terminal_js
|
||||
collapse_block = terminal_js.split("function collapseComposerTerminal", 1)[1].split("function expandComposerTerminal", 1)[0]
|
||||
assert "api('/api/terminal/close'" not in collapse_block
|
||||
assert "_disposeXterm" not in collapse_block
|
||||
assert "_setTerminalChromeState('collapsed')" in collapse_block
|
||||
expand_block = terminal_js.split("function expandComposerTerminal", 1)[1].split("function _disposeXterm", 1)[0]
|
||||
assert "_setTerminalChromeState('expanded')" in expand_block
|
||||
assert "_resetTerminalHeightForViewport();" in expand_block
|
||||
assert "focusComposerTerminalInput();" in expand_block
|
||||
close_block = terminal_js.split("async function closeComposerTerminal", 1)[1].split("async function restartComposerTerminal", 1)[0]
|
||||
assert "api('/api/terminal/close'" in close_block
|
||||
assert "_disposeXterm();" in close_block
|
||||
|
||||
|
||||
def test_terminal_slash_command_expands_existing_collapsed_terminal():
|
||||
commands_js = _read("static/commands.js")
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
assert "await toggleComposerTerminal(true)" in commands_js
|
||||
toggle_block = terminal_js.split("async function toggleComposerTerminal", 1)[1].split("function collapseComposerTerminal", 1)[0]
|
||||
assert "if(TERMINAL_UI.open)" in toggle_block
|
||||
assert "if(TERMINAL_UI.collapsed)expandComposerTerminal();" in toggle_block
|
||||
assert "else focusComposerTerminalInput();" in toggle_block
|
||||
|
||||
|
||||
def test_terminal_v1_does_not_expose_send_to_chat_action():
|
||||
html = _read("static/index.html")
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
Reference in New Issue
Block a user