Keep terminal card from covering transcript

This commit is contained in:
Frank Song
2026-04-28 10:53:01 +08:00
committed by Hermes Agent
parent 38c0912da1
commit 4575cae9db
3 changed files with 9 additions and 194 deletions
-1
View File
@@ -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
View File
@@ -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;
-72
View File
@@ -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")