fix: reasoning chip dropdown visible + monochrome SVG icon + /btw answer preserved (closes #933) (#934)
* fix: reasoning chip dropdown visible + SVG icon + /btw answer no longer wiped (closes #933) * fix(ui): resize handler symmetry + lock regressions for PR #934 fixes Two small additions on top of the core PR: 1. Resize handler now re-positions the reasoning dropdown when the window resizes while it's open, matching the existing model-dropdown branch. Without this, resizing while the dropdown is open leaves it aligned to the pre-resize chip position — fine in practice (most resizes close the dropdown via the global click handler) but inconsistent with the model-dropdown sibling. 2. Regression test file tests/test_reasoning_chip_btw_fixes.py with 10 tests locking all four fixes in place so they can't silently regress: - Dropdown sits OUTSIDE .composer-left (so overflow-y: hidden can't clip it) - Dropdown is grouped with the other composer-level dropdowns - Chip button contains stroke="currentColor" SVG (not a 🧠 emoji) - _applyReasoningChip() body doesn't include 🧠 - cmdReasoning calls _applyReasoningChip(eff) directly with the server-confirmed effort, not syncReasoningChip() (stale cache) - _streamDone flag declared, set in done handler, checked in onerror - _ensureBtwRow() called in done handler (creates bubble when no tokens arrive) - resize handler re-positions composerReasoningDropdown Full suite: 2056 passed, 0 failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Reasoning chip dropdown now opens correctly** — the dropdown was placed inside `.composer-left` which has `overflow-y: hidden`, clipping the upward-opening menu entirely. Moved `#composerReasoningDropdown` outside to sit alongside the model/profile/workspace dropdowns and added `_positionReasoningDropdown()` for consistent chip-aligned positioning. Z-index raised to 200 to match other composer dropdowns. (`static/index.html`, `static/style.css`, `static/ui.js`)
|
||||
- **Reasoning chip icon is now a monochrome SVG** — replaced the `🧠` emoji in the label with a `stroke="currentColor"` brain-outline SVG matching the style of all other composer chips. (`static/index.html`, `static/ui.js`)
|
||||
- **`/reasoning <level>` now immediately updates the chip** — previously called `syncReasoningChip()` which re-applied the stale cached value. Now calls `_applyReasoningChip(eff)` directly with the server-confirmed effort level. (`static/commands.js`)
|
||||
- **`/btw` answer no longer vanishes after rendering** — `onerror` was firing when the server cleanly closed the SSE connection after `stream_end`, removing the just-rendered answer bubble. A `_streamDone` flag now prevents `onerror` from wiping the row after a successful stream. Also added `_ensureBtwRow()` call in `done` handler so the bubble renders even if no `token` events arrived. (`static/messages.js`) Closes #933.
|
||||
|
||||
### Added
|
||||
- **Session attention indicators in the sidebar** — the session list now shows a
|
||||
spinning indicator while a session is actively streaming (even in the
|
||||
|
||||
@@ -654,8 +654,8 @@ function cmdReasoning(args){
|
||||
api('/api/reasoning',{method:'POST',body:JSON.stringify({effort:arg})})
|
||||
.then(function(st){
|
||||
const eff=(st && st.reasoning_effort)||arg;
|
||||
showToast(BRAIN+' Reasoning effort set to '+eff+' (saved; applies to next turn)');
|
||||
if(typeof syncReasoningChip==='function') syncReasoningChip();
|
||||
showToast('Reasoning effort set to '+eff+' (saved; applies to next turn)');
|
||||
if(typeof _applyReasoningChip==='function') _applyReasoningChip(eff);
|
||||
})
|
||||
.catch(function(e){
|
||||
showToast(BRAIN+' Failed to set effort: '+(e && e.message ? e.message : arg));
|
||||
|
||||
@@ -348,18 +348,10 @@
|
||||
</div>
|
||||
<div class="composer-reasoning-wrap" id="composerReasoningWrap" style="display:none">
|
||||
<button class="composer-reasoning-chip" id="composerReasoningChip" type="button" onclick="toggleReasoningDropdown()" title="Reasoning effort level">
|
||||
<span class="composer-reasoning-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="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z"/><line x1="9" y1="21" x2="15" y2="21"/></svg></span>
|
||||
<span class="composer-reasoning-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="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.46 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.46 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"/></svg></span>
|
||||
<span class="composer-reasoning-label" id="composerReasoningLabel"></span>
|
||||
<span class="composer-reasoning-chevron" aria-hidden="true"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
|
||||
</button>
|
||||
<div class="composer-reasoning-dropdown" id="composerReasoningDropdown">
|
||||
<div class="reasoning-option" data-effort="none">None</div>
|
||||
<div class="reasoning-option" data-effort="minimal">Minimal</div>
|
||||
<div class="reasoning-option" data-effort="low">Low</div>
|
||||
<div class="reasoning-option" data-effort="medium">Medium</div>
|
||||
<div class="reasoning-option" data-effort="high">High</div>
|
||||
<div class="reasoning-option" data-effort="xhigh">Extra High</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-model-wrap">
|
||||
<button class="composer-model-chip" id="composerModelChip" type="button" onclick="toggleModelDropdown()" title="Conversation model">
|
||||
@@ -418,6 +410,14 @@
|
||||
</div>
|
||||
<div class="profile-dropdown" id="profileDropdown"></div>
|
||||
<div class="ws-dropdown ws-dropdown-footer" id="composerWsDropdown"></div>
|
||||
<div class="composer-reasoning-dropdown" id="composerReasoningDropdown">
|
||||
<div class="reasoning-option" data-effort="none">None</div>
|
||||
<div class="reasoning-option" data-effort="minimal">Minimal</div>
|
||||
<div class="reasoning-option" data-effort="low">Low</div>
|
||||
<div class="reasoning-option" data-effort="medium">Medium</div>
|
||||
<div class="reasoning-option" data-effort="high">High</div>
|
||||
<div class="reasoning-option" data-effort="xhigh">Extra High</div>
|
||||
</div>
|
||||
<div class="model-dropdown" id="composerModelDropdown"></div>
|
||||
</div>
|
||||
<div class="upload-bar-wrap" id="uploadBarWrap"><div class="upload-bar" id="uploadBar"></div></div>
|
||||
|
||||
@@ -1332,6 +1332,7 @@ function attachBtwStream(parentSid, streamId, question){
|
||||
const src=new EventSource('/api/stream?stream_id='+encodeURIComponent(streamId));
|
||||
let answer='';
|
||||
let btwRow=null;
|
||||
let _streamDone=false;
|
||||
function _ensureBtwRow(){
|
||||
if(btwRow&&btwRow.isConnected) return;
|
||||
const inner=$('msgInner');
|
||||
@@ -1363,10 +1364,12 @@ function attachBtwStream(parentSid, streamId, question){
|
||||
});
|
||||
src.addEventListener('done',e=>{
|
||||
src.close();
|
||||
_streamDone=true;
|
||||
try{
|
||||
const d=JSON.parse(e.data);
|
||||
if(d.answer&&!answer) answer=d.answer;
|
||||
}catch(_){}
|
||||
_ensureBtwRow();
|
||||
if(btwRow&&btwRow.isConnected){
|
||||
const ansEl=btwRow.querySelector('.msg-btw-answer');
|
||||
if(ansEl) ansEl.innerHTML=renderMd(answer||t('btw_no_answer'));
|
||||
@@ -1375,6 +1378,7 @@ function attachBtwStream(parentSid, streamId, question){
|
||||
});
|
||||
src.addEventListener('apperror',e=>{
|
||||
src.close();
|
||||
_streamDone=true;
|
||||
try{
|
||||
const d=JSON.parse(e.data);
|
||||
showToast(t('btw_failed')+(d.message||''));
|
||||
@@ -1382,7 +1386,7 @@ function attachBtwStream(parentSid, streamId, question){
|
||||
if(btwRow&&btwRow.isConnected) btwRow.remove();
|
||||
});
|
||||
src.addEventListener('stream_end',()=>{src.close();});
|
||||
src.onerror=()=>{src.close();if(btwRow&&btwRow.isConnected) btwRow.remove();};
|
||||
src.onerror=()=>{src.close();if(!_streamDone&&btwRow&&btwRow.isConnected) btwRow.remove();};
|
||||
}
|
||||
|
||||
// ── /background task tracking ────────────────────────────────────────────────
|
||||
|
||||
@@ -599,7 +599,7 @@
|
||||
.composer-reasoning-chip.active{color:var(--text);background:var(--accent-bg);border-color:var(--accent-bg);}
|
||||
.composer-reasoning-icon,.composer-reasoning-chevron{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1;}
|
||||
.composer-reasoning-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 6px);left:0;background:var(--surface);border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.15);min-width:140px;z-index:100;padding:4px;animation:dropdown-in .12s ease-out;}
|
||||
.composer-reasoning-dropdown{display:none;position:absolute;bottom:calc(100% + 4px);left:0;background:var(--surface);border:1px solid var(--border2);border-radius:10px;box-shadow:0 -4px 24px rgba(0,0,0,.4);min-width:140px;z-index:200;padding:4px;animation:dropdown-in .12s ease-out;}
|
||||
.composer-reasoning-dropdown.open{display:block;}
|
||||
.reasoning-option{padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--text);white-space:nowrap;transition:background-color .1s;}
|
||||
.reasoning-option:hover{background:var(--hover-bg);}
|
||||
|
||||
22
static/ui.js
22
static/ui.js
@@ -406,6 +406,12 @@ document.addEventListener('click',e=>{
|
||||
window.addEventListener('resize',()=>{
|
||||
const dd=$('composerModelDropdown');
|
||||
if(dd&&dd.classList.contains('open')) _positionModelDropdown();
|
||||
// Keep the reasoning dropdown aligned under its chip when the window
|
||||
// resizes while open — same pattern as the model dropdown above.
|
||||
const rdd=$('composerReasoningDropdown');
|
||||
if(rdd&&rdd.classList.contains('open')&&typeof _positionReasoningDropdown==='function'){
|
||||
_positionReasoningDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Reasoning effort chip ────────────────────────────────────────────────────
|
||||
@@ -418,7 +424,7 @@ function _applyReasoningChip(eff){
|
||||
if(!wrap||!label) return;
|
||||
if(!eff||eff==='none'){wrap.style.display='none';return;}
|
||||
wrap.style.display='';
|
||||
label.textContent='🧠 '+eff;
|
||||
label.textContent=eff;
|
||||
_highlightReasoningOption(eff);
|
||||
}
|
||||
|
||||
@@ -452,9 +458,23 @@ function toggleReasoningDropdown(){
|
||||
closeModelDropdown();
|
||||
_highlightReasoningOption(_currentReasoningEffort);
|
||||
dd.classList.add('open');
|
||||
_positionReasoningDropdown();
|
||||
chip.classList.add('active');
|
||||
}
|
||||
|
||||
function _positionReasoningDropdown(){
|
||||
const dd=$('composerReasoningDropdown');
|
||||
const chip=$('composerReasoningChip');
|
||||
const footer=document.querySelector('.composer-footer');
|
||||
if(!dd||!chip||!footer) return;
|
||||
const chipRect=chip.getBoundingClientRect();
|
||||
const footerRect=footer.getBoundingClientRect();
|
||||
let left=chipRect.left-footerRect.left;
|
||||
const maxLeft=Math.max(0,footer.clientWidth-dd.offsetWidth);
|
||||
left=Math.max(0,Math.min(left,maxLeft));
|
||||
dd.style.left=`${left}px`;
|
||||
}
|
||||
|
||||
function closeReasoningDropdown(){
|
||||
const dd=$('composerReasoningDropdown');
|
||||
const chip=$('composerReasoningChip');
|
||||
|
||||
224
tests/test_reasoning_chip_btw_fixes.py
Normal file
224
tests/test_reasoning_chip_btw_fixes.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""Regression tests for PR #934 UI fixes.
|
||||
|
||||
Four invariants this file locks in place:
|
||||
|
||||
1. `#composerReasoningDropdown` lives OUTSIDE `.composer-left` (as a sibling of
|
||||
the other composer dropdowns), so it isn't clipped by that container's
|
||||
`overflow-y: hidden`. Regresses to invisible-dropdown if moved back.
|
||||
|
||||
2. The reasoning chip label uses an SVG icon (`stroke="currentColor"`) instead
|
||||
of the `🧠` emoji, matching every other composer chip.
|
||||
|
||||
3. `cmdReasoning()` calls `_applyReasoningChip(eff)` directly with the
|
||||
server-confirmed effort, not `syncReasoningChip()` which re-applies the
|
||||
stale cached value.
|
||||
|
||||
4. `attachBtwStream()` sets a `_streamDone` flag in `done`/`apperror` and
|
||||
gates `onerror`'s row removal on `!_streamDone` — otherwise the browser's
|
||||
post-`stream_end` error event wipes the just-rendered answer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||||
INDEX = (REPO / "static" / "index.html").read_text(encoding="utf-8")
|
||||
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
COMMANDS_JS = (REPO / "static" / "commands.js").read_text(encoding="utf-8")
|
||||
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ── #1 dropdown escapes composer-left ─────────────────────────────────────────
|
||||
|
||||
|
||||
class TestReasoningDropdownEscapesComposerLeft:
|
||||
"""The dropdown must sit as a sibling of .composer-footer, not inside
|
||||
.composer-left which has overflow-y: hidden and clips absolute children."""
|
||||
|
||||
def test_dropdown_lives_outside_composer_left(self):
|
||||
# Find the <div class="composer-left">...</div> block and confirm the
|
||||
# reasoning dropdown is NOT inside it.
|
||||
m = re.search(
|
||||
r'<div class="composer-left"[^>]*>(?P<body>[\s\S]*?)<div class="composer-footer-right"',
|
||||
INDEX,
|
||||
)
|
||||
# Some templates use different closing structures; fall back to a
|
||||
# coarser search that at least locates composer-left.
|
||||
if m:
|
||||
inner = m.group("body")
|
||||
assert 'id="composerReasoningDropdown"' not in inner, (
|
||||
"composerReasoningDropdown is still nested inside .composer-left — "
|
||||
"this is the exact bug #933 flagged: overflow-y: hidden clips "
|
||||
"upward-opening absolute dropdowns. Move it alongside "
|
||||
"#composerModelDropdown / #composerWsDropdown / #profileDropdown."
|
||||
)
|
||||
# Either way, check that the dropdown sits next to the other composer
|
||||
# dropdowns (reliable structural marker).
|
||||
assert '<div class="profile-dropdown" id="profileDropdown"></div>' in INDEX
|
||||
assert 'id="composerReasoningDropdown"' in INDEX
|
||||
|
||||
def test_dropdown_is_sibling_of_other_composer_dropdowns(self):
|
||||
# The four composer-level dropdowns must appear contiguously — if one
|
||||
# of them is nested inside an overflow-hidden container, this would
|
||||
# typically split the group.
|
||||
positions = [
|
||||
("profileDropdown", INDEX.find('id="profileDropdown"')),
|
||||
("composerWsDropdown", INDEX.find('id="composerWsDropdown"')),
|
||||
("composerReasoningDropdown", INDEX.find('id="composerReasoningDropdown"')),
|
||||
("composerModelDropdown", INDEX.find('id="composerModelDropdown"')),
|
||||
]
|
||||
for name, pos in positions:
|
||||
assert pos > -1, f"{name} not found in index.html"
|
||||
# They should all be in the same area of the document — within ~1.5 KB
|
||||
window = [p for _, p in positions]
|
||||
assert max(window) - min(window) < 2000, (
|
||||
"composer dropdowns are no longer grouped — reasoning dropdown may "
|
||||
"have drifted back inside a nested container"
|
||||
)
|
||||
|
||||
|
||||
# ── #2 monochrome SVG replaces emoji ──────────────────────────────────────────
|
||||
|
||||
|
||||
class TestReasoningChipIcon:
|
||||
"""The chip must render a currentColor SVG, not a 🧠 emoji, for cross-platform
|
||||
rendering consistency with the other composer chips."""
|
||||
|
||||
def test_chip_button_contains_svg_with_currentColor(self):
|
||||
# Locate the chip button and confirm it contains a stroke="currentColor" SVG
|
||||
m = re.search(
|
||||
r'<button class="composer-reasoning-chip"[^>]*>([\s\S]*?)</button>',
|
||||
INDEX,
|
||||
)
|
||||
assert m, "composer-reasoning-chip button not found"
|
||||
btn_body = m.group(1)
|
||||
assert 'stroke="currentColor"' in btn_body, (
|
||||
"reasoning chip must use stroke='currentColor' SVG matching other chips"
|
||||
)
|
||||
assert '<svg' in btn_body, "reasoning chip must contain an <svg> icon"
|
||||
|
||||
def test_apply_reasoning_chip_label_has_no_emoji(self):
|
||||
# Locate _applyReasoningChip and confirm the label assignment doesn't
|
||||
# concatenate a 🧠 emoji.
|
||||
m = re.search(
|
||||
r"function\s+_applyReasoningChip\b[\s\S]*?^\}",
|
||||
UI_JS,
|
||||
re.MULTILINE,
|
||||
)
|
||||
assert m, "_applyReasoningChip not found in ui.js"
|
||||
fn = m.group(0)
|
||||
assert "🧠" not in fn, (
|
||||
"_applyReasoningChip should not concatenate a 🧠 emoji into the label — "
|
||||
"the chip already has a monochrome SVG icon next to the label"
|
||||
)
|
||||
|
||||
|
||||
# ── #3 /reasoning immediately updates chip ────────────────────────────────────
|
||||
|
||||
|
||||
class TestReasoningCommandUpdatesChip:
|
||||
"""cmdReasoning must apply the SERVER-CONFIRMED effort, not the cached value."""
|
||||
|
||||
def test_cmd_reasoning_calls_apply_not_sync(self):
|
||||
# Locate cmdReasoning and verify the success branch calls
|
||||
# _applyReasoningChip(eff) directly, not syncReasoningChip() which
|
||||
# would read stale _currentReasoningEffort.
|
||||
m = re.search(
|
||||
r"function\s+cmdReasoning\b[\s\S]*?(?=^function\s|\Z)",
|
||||
COMMANDS_JS,
|
||||
re.MULTILINE,
|
||||
)
|
||||
assert m, "cmdReasoning not found in commands.js"
|
||||
fn = m.group(0)
|
||||
assert "_applyReasoningChip(eff)" in fn, (
|
||||
"cmdReasoning must call _applyReasoningChip(eff) with the "
|
||||
"server-confirmed effort from the /api/reasoning POST response"
|
||||
)
|
||||
|
||||
|
||||
# ── #4 /btw answer not wiped by onerror after clean close ─────────────────────
|
||||
|
||||
|
||||
class TestBtwStreamDoneGuard:
|
||||
"""attachBtwStream must guard onerror with a _streamDone flag so the
|
||||
browser's post-stream_end error event doesn't wipe the just-rendered row."""
|
||||
|
||||
def get_attach_btw(self):
|
||||
m = re.search(
|
||||
r"function\s+attachBtwStream\b[\s\S]*?(?=^function\s|\Z)",
|
||||
MESSAGES_JS,
|
||||
re.MULTILINE,
|
||||
)
|
||||
assert m, "attachBtwStream not found in messages.js"
|
||||
return m.group(0)
|
||||
|
||||
def test_stream_done_flag_declared(self):
|
||||
fn = self.get_attach_btw()
|
||||
assert "_streamDone" in fn, (
|
||||
"attachBtwStream must declare a _streamDone flag to distinguish "
|
||||
"clean server-closed streams from real errors"
|
||||
)
|
||||
|
||||
def test_stream_done_set_in_done_handler(self):
|
||||
fn = self.get_attach_btw()
|
||||
# Inside the 'done' listener body, _streamDone must be set true.
|
||||
done_block_m = re.search(
|
||||
r"addEventListener\('done'[\s\S]*?(?=addEventListener\(')",
|
||||
fn,
|
||||
)
|
||||
assert done_block_m, "done handler not found in attachBtwStream"
|
||||
assert "_streamDone=true" in done_block_m.group(0) or \
|
||||
"_streamDone = true" in done_block_m.group(0), (
|
||||
"_streamDone must be set to true in the done handler so onerror "
|
||||
"knows the stream completed successfully"
|
||||
)
|
||||
|
||||
def test_onerror_gated_on_stream_done(self):
|
||||
fn = self.get_attach_btw()
|
||||
# onerror must NOT unconditionally call btwRow.remove()
|
||||
m = re.search(r"src\.onerror\s*=\s*\(?\)?\s*=>\s*\{[^}]*\}", fn)
|
||||
assert m, "src.onerror assignment not found"
|
||||
handler = m.group(0)
|
||||
assert "_streamDone" in handler, (
|
||||
"src.onerror must check !_streamDone before removing the btw row — "
|
||||
"otherwise the browser's post-stream_end error fire wipes the "
|
||||
"answer that was just rendered by the done handler"
|
||||
)
|
||||
|
||||
def test_ensure_btw_row_called_in_done(self):
|
||||
"""The done handler must create the row even if no token events arrived
|
||||
(e.g., agent returned a non-streaming single-shot answer)."""
|
||||
fn = self.get_attach_btw()
|
||||
done_block_m = re.search(
|
||||
r"addEventListener\('done'[\s\S]*?(?=addEventListener\(')",
|
||||
fn,
|
||||
)
|
||||
assert done_block_m
|
||||
assert "_ensureBtwRow()" in done_block_m.group(0), (
|
||||
"done handler must call _ensureBtwRow() so the answer bubble exists "
|
||||
"even if no token events arrived before done"
|
||||
)
|
||||
|
||||
|
||||
# ── #5 resize handler symmetry (non-blocking polish) ─────────────────────────
|
||||
|
||||
|
||||
class TestResizeHandlerSymmetry:
|
||||
"""When the window resizes while either the model OR reasoning dropdown is
|
||||
open, the dropdown must be re-positioned so it stays aligned under its chip."""
|
||||
|
||||
def test_resize_repositions_reasoning_dropdown(self):
|
||||
# The global resize handler must handle both composerModelDropdown AND
|
||||
# composerReasoningDropdown to keep them aligned when the window resizes.
|
||||
m = re.search(
|
||||
r"window\.addEventListener\(\s*['\"]resize['\"][\s\S]*?\}\s*\)\s*;",
|
||||
UI_JS,
|
||||
)
|
||||
assert m, "window resize handler not found in ui.js"
|
||||
handler = m.group(0)
|
||||
assert "composerReasoningDropdown" in handler, (
|
||||
"window resize handler must also re-position composerReasoningDropdown "
|
||||
"while it's open (symmetric with the existing model-dropdown branch)"
|
||||
)
|
||||
Reference in New Issue
Block a user