diff --git a/static/style.css b/static/style.css index 691b6fe6..ebc68cea 100644 --- a/static/style.css +++ b/static/style.css @@ -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);} diff --git a/static/terminal.js b/static/terminal.js index 1dd7a998..9bebda3b 100644 --- a/static/terminal.js +++ b/static/terminal.js @@ -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; diff --git a/tests/test_embedded_workspace_terminal.py b/tests/test_embedded_workspace_terminal.py index 06590154..b7842be4 100644 --- a/tests/test_embedded_workspace_terminal.py +++ b/tests/test_embedded_workspace_terminal.py @@ -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")