diff --git a/static/commands.js b/static/commands.js index c09cbf54..c7d9cd52 100644 --- a/static/commands.js +++ b/static/commands.js @@ -875,6 +875,26 @@ async function cmdSteer(args){ * @param {boolean} explicitSteer - True if the user explicitly invoked /steer * (vs the busy-mode auto-fallback). Affects toast wording only. */ +function _showSteerIndicator(text){ + const inner=document.getElementById('msgInner'); + if(!inner) return; + // Remove any existing steer indicator + const old=inner.querySelector('.steer-indicator'); + if(old) old.remove(); + const el=document.createElement('div'); + el.className='steer-indicator'; + const badge=document.createElement('span'); + badge.className='steer-badge'; + badge.textContent='Steer'; + const body=document.createElement('span'); + body.className='steer-body'; + body.textContent=text.length>120?text.slice(0,117)+'…':text; + el.appendChild(badge); + el.appendChild(body); + inner.appendChild(el); + if(typeof scrollToBottom==='function') scrollToBottom(); +} + async function _trySteer(msg, explicitSteer){ let result=null; try{ @@ -887,6 +907,11 @@ async function _trySteer(msg, explicitSteer){ result={accepted:false, fallback:'network_error'}; } if(result&&result.accepted){ + // Show a transient steer indicator in the chat (NOT in S.messages — it must + // survive the done event's S.messages=d.session.messages replacement). + // The indicator self-removes when the turn completes (done/cancel/error + // all call renderMessages which rebuilds msgInner). + _showSteerIndicator(msg); showToast(t('cmd_steer_delivered'),2500); return; } diff --git a/static/style.css b/static/style.css index 98661718..7b463d4c 100644 --- a/static/style.css +++ b/static/style.css @@ -796,6 +796,10 @@ @media(min-width:1800px){.messages-inner{max-width:1200px;}} .msg-row{padding:10px 0;} .msg-row+.msg-row{border-top:none;} + /* Steer indicator: transient banner below messages, removed on renderMessages rebuild */ + .steer-indicator{display:flex;align-items:baseline;gap:8px;padding:10px 0;opacity:.65;font-style:italic;color:var(--accent-text);} + .steer-indicator .steer-body{white-space:pre-wrap;word-break:break-word;} + .steer-badge{display:inline-block;font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--accent-text);background:var(--accent-bg);border:1px solid var(--accent-bg-strong);border-radius:4px;padding:1px 6px;vertical-align:middle;line-height:1.6;font-style:normal;flex-shrink:0;} .msg-role{font-size:12px;font-weight:500;letter-spacing:.01em;margin-bottom:8px;display:flex;align-items:center;gap:8px;} .msg-role.user{color:var(--accent);} .msg-role.assistant{color:var(--accent-text);opacity:.6;} diff --git a/tests/test_1062_busy_input_modes.py b/tests/test_1062_busy_input_modes.py index 934dac8b..47ede9ab 100644 --- a/tests/test_1062_busy_input_modes.py +++ b/tests/test_1062_busy_input_modes.py @@ -106,7 +106,7 @@ class TestSlashCommandHandlers: # The shared helper must contain the fallback path helper_idx = COMMANDS_JS.find("async function _trySteer(") assert helper_idx >= 0, "_trySteer helper must exist" - helper_body = COMMANDS_JS[helper_idx:helper_idx + 1500] + helper_body = COMMANDS_JS[helper_idx:helper_idx + 2000] assert "queueSessionMessage" in helper_body assert "cancelStream" in helper_body # Toast should differ from interrupt to signal it's the steer path @@ -139,7 +139,7 @@ class TestSlashCommandHandlers: # cmdSteer delegates to _trySteer; that helper clears pendingFiles idx_try = COMMANDS_JS.find("function _trySteer(") assert idx_try >= 0, "_trySteer not found" - try_body = COMMANDS_JS[idx_try:idx_try + 1200] + try_body = COMMANDS_JS[idx_try:idx_try + 1600] assert "S.pendingFiles=[]" in try_body, ( "_trySteer must clear S.pendingFiles in its fallback path — " "without this, files are lost on steer→interrupt fallback"