From a8816fe22f4b864774b8897431fdd90bc0564a9c Mon Sep 17 00:00:00 2001 From: fxd-jason Date: Wed, 13 May 2026 20:30:44 +0800 Subject: [PATCH 1/2] feat: show steer indicator as transient DOM element When busy_input_mode is 'steer' and the steer is accepted by the server, show a transient indicator in the chat area (not in S.messages). This mirrors the CLI/Gateway approach: steer text is never stored in the message array. The done event's S.messages=d.session.messages replacement therefore doesn't cause a flash where all SSE content vanishes and re-appears. The indicator is an independent DOM element (.steer-indicator) appended to msgInner. It naturally disappears when renderMessages rebuilds msgInner on turn completion (done/cancel/error). --- static/commands.js | 25 +++++++++++++++++++++++++ static/style.css | 4 ++++ 2 files changed, 29 insertions(+) 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;} From 2567242e2fe6c35cbd91abe6ceb042adb7ef27e4 Mon Sep 17 00:00:00 2001 From: fxd-jason Date: Wed, 13 May 2026 20:30:53 +0800 Subject: [PATCH 2/2] test: widen _trySteer capture windows for steer indicator code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _showSteerIndicator function added before _trySteer extends the total capture region. Widen helper_body 1500→2000 and try_body 1200→1600 so assertions on cmd_steer_fallback and S.pendingFiles=[] still land within the window. --- tests/test_1062_busy_input_modes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_1062_busy_input_modes.py b/tests/test_1062_busy_input_modes.py index bb7514d2..3da04f6c 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"