Refine embedded terminal card entrypoint
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Embedded workspace terminal prototype** — composer now has a terminal toggle next to the workspace control. It opens a compact terminal panel bound to the current session workspace, with live output, command input, resize, restart, clear, copy output, and close actions. Backend terminal processes use explicit per-process `cwd`/`env` and do not mutate global `os.environ`. (`api/terminal.py`, `api/routes.py`, `static/index.html`, `static/terminal.js`, `static/style.css`, `static/i18n.js`)
|
||||
- **Embedded workspace terminal prototype** — `/terminal` opens a compact utility card tucked behind the composer, following the approval-card layering pattern instead of adding a permanent composer icon or full-width dock. The terminal stays bound to the current session workspace with live output, command input, resize, restart, clear, copy output, and close actions. Backend terminal processes use explicit per-process `cwd`/`env` and do not mutate global `os.environ`. (`api/terminal.py`, `api/routes.py`, `static/index.html`, `static/terminal.js`, `static/commands.js`, `static/style.css`, `static/i18n.js`)
|
||||
|
||||
### Fixed
|
||||
- **Background session completion unread dots** — sidebar unread dots no longer
|
||||
|
||||
@@ -11,6 +11,7 @@ const COMMANDS=[
|
||||
{name:'compact', desc:t('cmd_compact_alias'), fn:cmdCompact, noEcho:true},
|
||||
{name:'model', desc:t('cmd_model'), fn:cmdModel, arg:'model_name', subArgs:'models', noEcho:true},
|
||||
{name:'workspace', desc:t('cmd_workspace'), fn:cmdWorkspace, arg:'name', noEcho:true},
|
||||
{name:'terminal', desc:t('cmd_terminal'), fn:cmdTerminal, noEcho:true},
|
||||
{name:'new', desc:t('cmd_new'), fn:cmdNew, noEcho:true},
|
||||
{name:'usage', desc:t('cmd_usage'), fn:cmdUsage, noEcho:true},
|
||||
{name:'theme', desc:t('cmd_theme'), fn:cmdTheme, arg:'name', noEcho:true},
|
||||
@@ -262,6 +263,26 @@ async function cmdWorkspace(args){
|
||||
}catch(e){showToast(t('workspace_switch_failed')+e.message);}
|
||||
}
|
||||
|
||||
async function cmdTerminal(){
|
||||
if(!S.session&&typeof newSession==='function'){
|
||||
if(!S._profileSwitchWorkspace&&!S._profileDefaultWorkspace){
|
||||
try{
|
||||
const data=await api('/api/workspaces');
|
||||
const first=(data.workspaces||[])[0];
|
||||
S._profileSwitchWorkspace=data.last||(first&&first.path)||null;
|
||||
}catch(_){}
|
||||
}
|
||||
await newSession();
|
||||
if(typeof renderSessionList==='function') await renderSessionList();
|
||||
}
|
||||
if(!S.session||!S.session.workspace){
|
||||
showToast(t('terminal_no_workspace_title'),2600,'warning');
|
||||
if(typeof syncTerminalButton==='function') syncTerminalButton();
|
||||
return;
|
||||
}
|
||||
if(typeof toggleComposerTerminal==='function') await toggleComposerTerminal(true);
|
||||
}
|
||||
|
||||
async function cmdNew(){
|
||||
if(typeof clearCompressionUi==='function') clearCompressionUi();
|
||||
await newSession();
|
||||
|
||||
@@ -105,6 +105,7 @@ const LOCALES = {
|
||||
cmd_compact_alias: 'Legacy alias for /compress',
|
||||
cmd_model: 'Switch model (e.g. /model gpt-4o)',
|
||||
cmd_workspace: 'Switch workspace by name',
|
||||
cmd_terminal: 'Open the workspace terminal',
|
||||
cmd_new: 'Start a new chat session',
|
||||
cmd_usage: 'Toggle token usage display on/off',
|
||||
cmd_theme: 'Switch appearance (theme: system/dark/light, skin: default/ares/mono/slate/poseidon/sisyphus/charizard)',
|
||||
|
||||
@@ -314,7 +314,6 @@
|
||||
</div>
|
||||
<div class="composer-terminal-panel" id="composerTerminalPanel" hidden>
|
||||
<div class="composer-terminal-inner">
|
||||
<div class="composer-terminal-resize-handle" id="terminalResizeHandle" role="separator" aria-orientation="horizontal" aria-label="Resize terminal" tabindex="0"></div>
|
||||
<div class="composer-terminal-header">
|
||||
<div class="composer-terminal-title">
|
||||
<span data-i18n="terminal_title">Terminal</span>
|
||||
@@ -325,7 +324,6 @@
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalClear" onclick="clearComposerTerminal()" data-i18n="terminal_clear">Clear</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalCopy" onclick="copyComposerTerminalOutput()" data-i18n="terminal_copy_output">Copy output</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalRestart" onclick="restartComposerTerminal()" data-i18n="terminal_restart">Restart</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalCollapse" onclick="collapseComposerTerminal()" data-i18n="terminal_collapse">Collapse</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalClose" onclick="closeComposerTerminal()" data-i18n="terminal_close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,18 +331,6 @@
|
||||
<div class="composer-terminal-surface" id="terminalSurface" aria-label="Workspace terminal"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-terminal-dock" id="composerTerminalDock" hidden>
|
||||
<div class="composer-terminal-dock-title">
|
||||
<span class="composer-terminal-dock-dot" aria-hidden="true"></span>
|
||||
<span data-i18n="terminal_title">Terminal</span>
|
||||
<span class="composer-terminal-dot" aria-hidden="true">·</span>
|
||||
<span id="terminalDockWorkspaceLabel"></span>
|
||||
</div>
|
||||
<div class="composer-terminal-actions">
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalExpand" onclick="expandComposerTerminal()" data-i18n="terminal_expand">Expand</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalDockClose" onclick="closeComposerTerminal()" data-i18n="terminal_close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Queue pill outer: same positioning wrapper as .queue-card (max-width + padding) -->
|
||||
@@ -388,9 +374,6 @@
|
||||
</div>
|
||||
<div class="composer-ws-wrap">
|
||||
<div class="composer-workspace-group ws-chip" id="composerWorkspaceGroup" role="group" aria-label="Workspace controls">
|
||||
<button class="composer-workspace-terminal-btn" id="btnTerminalToggle" type="button" onclick="toggleComposerTerminal()" title="Open workspace terminal" data-i18n-title="terminal_open_title" aria-pressed="false" aria-label="Open workspace terminal">
|
||||
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg></span>
|
||||
</button>
|
||||
<button class="composer-workspace-files-btn" id="btnWorkspacePanelToggle" type="button" onclick="toggleWorkspacePanel()" title="Show workspace panel" aria-pressed="false" aria-label="Toggle workspace files panel">
|
||||
<span class="composer-workspace-icon" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>
|
||||
</button>
|
||||
@@ -478,24 +461,6 @@
|
||||
</div>
|
||||
<div class="upload-bar-wrap" id="uploadBarWrap"><div class="upload-bar" id="uploadBar"></div></div>
|
||||
</div>
|
||||
<div class="composer-terminal-panel" id="composerTerminalPanel" hidden>
|
||||
<div class="composer-terminal-header">
|
||||
<div class="composer-terminal-title">
|
||||
<span data-i18n="terminal_title">Terminal</span>
|
||||
<span class="composer-terminal-dot" aria-hidden="true">·</span>
|
||||
<span id="terminalWorkspaceLabel"></span>
|
||||
</div>
|
||||
<div class="composer-terminal-actions">
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalClear" onclick="clearComposerTerminal()" data-i18n="terminal_clear">Clear</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalCopy" onclick="copyComposerTerminalOutput()" data-i18n="terminal_copy_output">Copy output</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalRestart" onclick="restartComposerTerminal()" data-i18n="terminal_restart">Restart</button>
|
||||
<button type="button" class="composer-terminal-action" id="btnTerminalClose" onclick="closeComposerTerminal()" data-i18n="terminal_close">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-terminal-viewport" id="terminalViewport" onclick="focusComposerTerminalInput()">
|
||||
<div class="composer-terminal-surface" id="terminalSurface" aria-label="Workspace terminal"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /#mainChat -->
|
||||
<div id="mainSkills" class="main-view">
|
||||
|
||||
@@ -53,7 +53,7 @@ async function send(){
|
||||
// cmdSteer / cmdInterrupt say "No active task to stop."
|
||||
if(text.startsWith('/')){
|
||||
const _pc=typeof parseCommand==='function'&&parseCommand(text);
|
||||
if(_pc&&['steer','interrupt','queue'].includes(_pc.name)){
|
||||
if(_pc&&['steer','interrupt','queue','terminal'].includes(_pc.name)){
|
||||
const _bc=COMMANDS.find(c=>c.name===_pc.name);
|
||||
if(_bc){
|
||||
$('msg').value='';autoResize();
|
||||
|
||||
@@ -716,17 +716,12 @@
|
||||
.composer-workspace-group{display:inline-flex;align-items:stretch;max-width:284px;border-radius:999px;overflow:hidden;background-color:transparent;border:1px solid var(--border2);transition:background-color .15s,border-color .15s;}
|
||||
.composer-workspace-group:hover{background-color:var(--hover-bg);}
|
||||
.composer-workspace-group:hover{border-color:var(--border2);}
|
||||
.composer-workspace-group:hover .composer-workspace-terminal-btn,
|
||||
.composer-workspace-group:hover .composer-workspace-files-btn,
|
||||
.composer-workspace-group:hover .composer-workspace-chip{color:var(--text);}
|
||||
.composer-workspace-terminal-btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 10px 8px 12px;background-color:transparent;border:none;border-radius:999px 0 0 999px;color:var(--muted);cursor:pointer;transition:color .15s,background-color .15s;-webkit-tap-highlight-color:transparent;}
|
||||
.composer-workspace-terminal-btn:disabled{opacity:.45;cursor:not-allowed;}
|
||||
.composer-workspace-terminal-btn.active{color:var(--accent-text);background:var(--accent-bg);}
|
||||
.composer-workspace-files-btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 10px;background-color:transparent;border:none;border-left:1px solid transparent;border-radius:0;color:var(--muted);cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;-webkit-tap-highlight-color:transparent;}
|
||||
.composer-workspace-files-btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 10px 8px 12px;background-color:transparent;border:none;border-left:1px solid transparent;border-radius:999px 0 0 999px;color:var(--muted);cursor:pointer;transition:color .15s,background-color .15s,border-color .15s;-webkit-tap-highlight-color:transparent;}
|
||||
.composer-workspace-files-btn:disabled{opacity:.45;cursor:not-allowed;}
|
||||
.composer-workspace-files-btn.active{color:var(--accent-text);background:var(--accent-bg);}
|
||||
.composer-workspace-chip{display:inline-flex;align-items:center;gap:8px;min-width:0;max-width:200px;padding:8px 12px 8px 10px;border:none;border-left:1px solid transparent;border-radius:0 999px 999px 0;background-color:transparent;color:var(--muted);font-weight:500;cursor:pointer;transition:color .15s,border-color .15s;}
|
||||
.composer-workspace-group:hover .composer-workspace-terminal-btn+.composer-workspace-files-btn,
|
||||
.composer-workspace-group:hover .composer-workspace-files-btn+.composer-workspace-chip{border-left-color:var(--border2);}
|
||||
.composer-workspace-chip:disabled{opacity:.45;cursor:not-allowed;}
|
||||
.composer-workspace-chip.active{color:var(--text);background:var(--accent-bg);}
|
||||
@@ -798,14 +793,10 @@
|
||||
.send-btn:disabled{opacity:.35;cursor:not-allowed;transform:none;box-shadow:none;}
|
||||
.send-btn.visible{animation:send-pop-in .18s cubic-bezier(.34,1.56,.64,1) forwards;}
|
||||
.composer-terminal-panel{position:absolute;left:0;right:0;bottom:-24px;width:min(calc(100% - 64px),720px);margin:0 auto;box-sizing:border-box;overflow:hidden;pointer-events:none;z-index:1;}
|
||||
.composer-terminal-panel.is-open,.composer-terminal-panel.is-collapsed{pointer-events:auto;}
|
||||
.composer-terminal-panel.is-open{pointer-events:auto;}
|
||||
.composer-terminal-panel[hidden]{display:none!important;}
|
||||
.composer-terminal-inner{height:var(--composer-terminal-height,260px);min-height:180px;max-height:min(520px,50vh);display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border2);border-radius:14px;background:var(--surface);box-shadow:0 12px 32px rgba(0,0,0,.22);padding-bottom:38px;transform:translateY(100%);opacity:0;transition:transform .4s cubic-bezier(.32,.72,.16,1),opacity .25s ease;}
|
||||
.composer-terminal-inner{height:260px;min-height:180px;display:flex;flex-direction:column;overflow:hidden;resize:vertical;border:1px solid var(--border2);border-radius:14px;background:var(--surface);box-shadow:0 12px 32px rgba(0,0,0,.22);padding-bottom:38px;transform:translateY(100%);opacity:0;transition:transform .4s cubic-bezier(.32,.72,.16,1),opacity .25s ease;}
|
||||
.composer-terminal-panel.is-open .composer-terminal-inner{transform:translateY(0);opacity:1;}
|
||||
.composer-terminal-resize-handle{height:12px;display:flex;align-items:center;justify-content:center;flex:0 0 auto;cursor:ns-resize;touch-action:none;background:linear-gradient(to bottom,rgba(255,255,255,.04),transparent);}
|
||||
.composer-terminal-resize-handle::before{content:"";width:52px;height:4px;border-radius:999px;background:var(--border2);opacity:.72;transition:opacity .15s,background .15s;}
|
||||
.composer-terminal-resize-handle:hover::before,.composer-terminal-resize-handle:focus-visible::before{opacity:1;background:var(--muted);}
|
||||
.composer-terminal-inner.is-resizing{transition:none;user-select:none;}
|
||||
.composer-terminal-header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;border-bottom:1px solid var(--border);background:rgba(255,255,255,.025);}
|
||||
.composer-terminal-title{min-width:0;display:flex;align-items:center;gap:6px;color:var(--text);font-size:12px;font-weight:700;letter-spacing:.02em;text-transform:uppercase;}
|
||||
.composer-terminal-dot{color:var(--muted);font-weight:400;}
|
||||
@@ -973,7 +964,6 @@
|
||||
.composer-reasoning-chip{max-width:44px;min-width:44px;min-height:44px;padding:6px;justify-content:center;gap:0;font-size:11px;}
|
||||
/* Workspace group: keep split layout on mobile — files icon + chevron-only picker */
|
||||
.composer-workspace-group{min-height:44px;}
|
||||
.composer-workspace-terminal-btn{min-width:44px;padding:6px 8px;}
|
||||
.composer-workspace-files-btn{min-width:44px;padding:6px 8px;}
|
||||
.composer-workspace-chip{min-width:32px;padding:6px 8px;gap:0;}
|
||||
.composer-divider{display:none;}
|
||||
@@ -982,8 +972,7 @@
|
||||
.ctx-indicator{width:32px;height:32px;}
|
||||
.ctx-tooltip{right:-4px;min-width:190px;max-width:220px;}
|
||||
.composer-terminal-panel{width:calc(100% - 20px);}
|
||||
.composer-terminal-inner{height:var(--composer-terminal-height,190px);min-height:140px;max-height:min(300px,44vh);border-radius:12px;padding-bottom:28px;}
|
||||
.composer-terminal-resize-handle{height:10px;cursor:default;}
|
||||
.composer-terminal-inner{height:190px;min-height:140px;border-radius:12px;padding-bottom:28px;}
|
||||
.composer-terminal-header{padding:7px 8px;}
|
||||
.composer-terminal-actions{gap:2px;overflow-x:auto;}
|
||||
.composer-terminal-action{padding:5px 7px;font-size:10px;white-space:nowrap;}
|
||||
|
||||
@@ -28,7 +28,6 @@ function _terminalEls(){
|
||||
return {
|
||||
panel:$('composerTerminalPanel'),
|
||||
inner:$('composerTerminalPanel')&&$('composerTerminalPanel').querySelector('.composer-terminal-inner'),
|
||||
handle:$('terminalResizeHandle'),
|
||||
viewport:$('terminalViewport'),
|
||||
surface:$('terminalSurface'),
|
||||
toggle:$('btnTerminalToggle'),
|
||||
@@ -332,12 +331,12 @@ function _setTerminalChromeState(state){
|
||||
|
||||
function syncTerminalButton(){
|
||||
const {toggle}= _terminalEls();
|
||||
if(!toggle)return;
|
||||
const currentSid=_terminalSessionId();
|
||||
const currentWorkspace=S.session&&S.session.workspace;
|
||||
if(TERMINAL_UI.open&&TERMINAL_UI.sessionId&&(currentSid!==TERMINAL_UI.sessionId||currentWorkspace!==TERMINAL_UI.workspace)){
|
||||
closeComposerTerminal(TERMINAL_UI.sessionId);
|
||||
}
|
||||
if(!toggle)return;
|
||||
const hasWorkspace=!!(S.session&&S.session.workspace);
|
||||
toggle.disabled=!hasWorkspace;
|
||||
toggle.classList.toggle('active',TERMINAL_UI.open);
|
||||
@@ -412,13 +411,7 @@ async function _startComposerTerminal(restart=false){
|
||||
async function toggleComposerTerminal(force){
|
||||
const next=typeof force==='boolean'?force:!TERMINAL_UI.open;
|
||||
if(next){
|
||||
if(TERMINAL_UI.open){
|
||||
if(TERMINAL_UI.collapsed)expandComposerTerminal();
|
||||
else focusComposerTerminalInput();
|
||||
return;
|
||||
}
|
||||
const {panel,inner}= _terminalEls();
|
||||
const messages=_terminalMessagesEl();
|
||||
const {panel,inner,workspace}= _terminalEls();
|
||||
if(!panel)return;
|
||||
clearTimeout(TERMINAL_UI.closeTimer);
|
||||
panel.hidden=false;
|
||||
@@ -440,7 +433,7 @@ async function toggleComposerTerminal(force){
|
||||
syncTerminalButton();
|
||||
if(!TERMINAL_UI.resizeObserver&&window.ResizeObserver){
|
||||
TERMINAL_UI.resizeObserver=new ResizeObserver(()=>_fitTerminal());
|
||||
TERMINAL_UI.resizeObserver.observe(panel);
|
||||
TERMINAL_UI.resizeObserver.observe(inner||panel);
|
||||
}
|
||||
try{
|
||||
await _startComposerTerminal(false);
|
||||
|
||||
@@ -38,3 +38,15 @@ def test_approval_card_visible_outranks_queue_card():
|
||||
f"greater than .queue-card z-index ({queue_z}) so approval buttons "
|
||||
f"remain clickable when both flyouts are open."
|
||||
)
|
||||
|
||||
|
||||
def test_approval_card_visible_outranks_terminal_card():
|
||||
terminal_z = _z_index_of(r"\.composer-terminal-panel")
|
||||
approval_visible_z = _z_index_of(r"\.approval-card\.visible")
|
||||
assert terminal_z is not None, ".composer-terminal-panel must declare a z-index"
|
||||
assert approval_visible_z is not None
|
||||
assert approval_visible_z > terminal_z, (
|
||||
f".approval-card.visible z-index ({approval_visible_z}) must stay above "
|
||||
f".composer-terminal-panel z-index ({terminal_z}) so approval controls "
|
||||
f"remain clickable when the terminal flyout is open."
|
||||
)
|
||||
|
||||
@@ -9,13 +9,18 @@ def _read(path: str) -> str:
|
||||
return (REPO_ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_composer_has_workspace_terminal_entrypoint():
|
||||
def test_terminal_is_opened_by_slash_command_not_permanent_composer_icon():
|
||||
html = _read("static/index.html")
|
||||
commands_js = _read("static/commands.js")
|
||||
sw = _read("static/sw.js")
|
||||
assert 'id="btnTerminalToggle"' in html
|
||||
assert 'id="btnTerminalToggle"' not in html
|
||||
assert "name:'terminal'" in commands_js
|
||||
assert "fn:cmdTerminal" in commands_js
|
||||
assert "api('/api/workspaces')" in commands_js
|
||||
assert "await newSession()" in commands_js
|
||||
assert "toggleComposerTerminal(true)" in commands_js
|
||||
assert 'id="terminalViewport"' in html
|
||||
assert 'id="terminalSurface"' in html
|
||||
assert 'onclick="toggleComposerTerminal()"' in html
|
||||
assert 'static/terminal.js' in html
|
||||
assert './static/terminal.js' in sw
|
||||
assert "xterm@5.3.0" in html
|
||||
@@ -28,123 +33,14 @@ def test_terminal_surface_uses_composer_flyout_card_pattern():
|
||||
flyout = html.split('<div class="composer-flyout">', 1)[1].split('<div class="queue-pill-outer">', 1)[0]
|
||||
assert 'id="composerTerminalPanel"' in flyout
|
||||
assert 'class="composer-terminal-inner"' in flyout
|
||||
assert 'id="terminalResizeHandle"' in flyout
|
||||
assert 'id="composerTerminalPanel"' not in html.split('<div class="queue-pill-outer">', 1)[1]
|
||||
assert ".composer-terminal-panel{position:absolute" in style_css
|
||||
assert "bottom:-24px" in style_css
|
||||
assert "width:min(calc(100% - 64px),720px)" in style_css
|
||||
assert ".composer-wrap.terminal-dock-visible .composer-flyout{z-index:4" in style_css
|
||||
assert ".composer-terminal-panel.is-collapsed{bottom:-2px;width:min(calc(100% - 112px),560px);overflow:visible;z-index:4" in style_css
|
||||
assert ".composer-terminal-panel.is-expanding-from-dock .composer-terminal-inner{transition:opacity .18s ease" in style_css
|
||||
assert ".messages.terminal-expanding-from-dock{transition:none!important" in style_css
|
||||
assert ".composer-terminal-dock{min-height:42px" in style_css
|
||||
assert ".composer-terminal-inner{height:var(--composer-terminal-height,260px)" in style_css
|
||||
assert ".composer-terminal-inner{height:260px" in style_css
|
||||
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 "scroll-padding-bottom:var(--terminal-card-height" in style_css
|
||||
assert "classList.add('terminal-open')" in terminal_js
|
||||
assert "classList.remove('terminal-open')" in terminal_js
|
||||
assert "messages.style.setProperty('--terminal-card-height'" in terminal_js
|
||||
assert "messages.style.removeProperty('--terminal-card-height')" in terminal_js
|
||||
assert "function _terminalIsMessagesNearBottom" in terminal_js
|
||||
assert "scrollToBottom" in terminal_js
|
||||
|
||||
|
||||
def test_terminal_initial_open_settles_transcript_space_before_reveal():
|
||||
terminal_js = _read("static/terminal.js")
|
||||
|
||||
open_block = terminal_js.split("async function toggleComposerTerminal", 1)[1].split("function collapseComposerTerminal", 1)[0]
|
||||
assert "messages.classList.add('terminal-expanding-from-dock')" in open_block
|
||||
assert "_syncTerminalTranscriptSpace(true,{immediate:true});" in open_block
|
||||
assert "void messages.offsetHeight;" in open_block
|
||||
assert "panel.classList.add('is-open')" in open_block
|
||||
assert "messages.classList.remove('terminal-expanding-from-dock')" in open_block
|
||||
assert open_block.index("_syncTerminalTranscriptSpace(true,{immediate:true});") < open_block.index("panel.classList.add('is-open')")
|
||||
assert open_block.index("void messages.offsetHeight;") < open_block.index("panel.classList.add('is-open')")
|
||||
|
||||
|
||||
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
|
||||
assert "composerWrap.classList.toggle('terminal-dock-visible',collapsed)" in terminal_js
|
||||
expand_block = terminal_js.split("function expandComposerTerminal", 1)[1].split("function _disposeXterm", 1)[0]
|
||||
assert "_setTerminalChromeState('expanded')" in expand_block
|
||||
assert "panel.classList.add('is-expanding-from-dock')" in expand_block
|
||||
assert "panel.classList.remove('is-expanding-from-dock')" in expand_block
|
||||
assert "messages.classList.add('terminal-expanding-from-dock')" in expand_block
|
||||
assert "messages.classList.remove('terminal-expanding-from-dock')" in expand_block
|
||||
assert "_syncTerminalTranscriptSpace(true,{immediate:true});" in expand_block
|
||||
assert "void messages.offsetHeight;" in expand_block
|
||||
assert expand_block.index("_syncTerminalTranscriptSpace(true,{immediate:true});") < expand_block.index("_setTerminalChromeState('expanded')")
|
||||
assert expand_block.index("void messages.offsetHeight;") < expand_block.index("_setTerminalChromeState('expanded')")
|
||||
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