From bc4cd77a528ef72cc3dd495051ee12464538c4b2 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 03:36:06 +0000 Subject: [PATCH] Fix sidechat selectors from DOM investigation\n\n- + button: [data-testid=hatch-chat-compose] (SVG, no text)\n- Panel: [data-testid=hatch-chat-switcher-trigger] (idempotent)\n- ensure_sidebar: check + button presence, not text (was false-positive\n on DM text containing Side chats) --- bin/muse-chat-api.py | 56 +++++++++++++---------------------------- bin/sidechat_manager.py | 27 +++++++++----------- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/bin/muse-chat-api.py b/bin/muse-chat-api.py index f13faaa..8b025af 100755 --- a/bin/muse-chat-api.py +++ b/bin/muse-chat-api.py @@ -263,51 +263,29 @@ def cmd_sidechat_list(ws): print(chats[:500]) def cmd_sidechat_create(ws): - """Create a new side chat via the '+' button next to 'Side chats' header. + """Create a new side chat via the '+' button. Returns the new thread URL. - Note: Ctrl+J opens the chat panel (which contains Side chats). - Fixed 2026-10-04: sidebar is actually the chat panel, toggled by Ctrl+J. + Selectors (verified 2026-10-04 via DOM investigation): + - Panel opener: [data-testid="hatch-chat-switcher-trigger"] (idempotent) + - + button: [data-testid="hatch-chat-compose"] (SVG icon, no text) """ import time as _time - # Chat panel is always open in the React app - no toggle needed. - # Ctrl+J would CLOSE it (it's a toggle). Just proceed to find + button. - # Fixed 2026-10-04: removed Ctrl+J, it was closing the always-open panel. - # The + button sits next to the "Side chats" header (see screenshot 2026-10-04). - # It's not labeled "New side chat" - find it by proximity to the header text. - # Wait for React to render the sidebar content first (up to 10s). - for _wait in range(10): - _has_header = ev(ws, "document.body.innerText.includes('Side chats')") - if _has_header: - break - _time.sleep(1) + # Open chat panel via switcher trigger (idempotent - safe if already open) + ev(ws, """(() => { + const sw = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'); + if (sw) sw.click(); + return sw ? 'SWITCHER_CLICKED' : 'NO_SWITCHER'; + })()""") + _time.sleep(2) # Wait for React render + # Find + button via data-testid (verified selector, SVG icon no text) result = ev(ws, """(() => { - // Strategy 1: Find button with "+" text near "Side chats" - const header = [...document.querySelectorAll('*')].find(el => - el.textContent.trim() === 'Side chats' && el.children.length === 0); - if (header) { - // Look for + button in the header's parent or siblings - const parent = header.parentElement; - if (parent) { - const plus = [...parent.querySelectorAll('button')].find(b => - (b.textContent||'').includes('+') || (b.getAttribute('aria-label')||'').toLowerCase().includes('add')); - if (plus) { plus.click(); return 'CLICKED_VIA_HEADER'; } - } - } - // Strategy 2: Find any button with "+" that's near the top - const plusBtn = [...document.querySelectorAll('button')].find(b => { - const t = (b.textContent||'').trim(); - const al = (b.getAttribute('aria-label')||'').toLowerCase(); - return t === '+' || al.includes('new side') || al.includes('add chat'); - }); - if (plusBtn) { plusBtn.click(); return 'CLICKED_VIA_PLUS'; } - // Strategy 3 (fallback): old aria-label exact match - const oldBtn = [...document.querySelectorAll('button')].find(b => - (b.getAttribute('aria-label')||'') === 'New side chat'); - if (oldBtn) { oldBtn.click(); return 'CLICKED_VIA_ARIA'; } - return 'NOT_FOUND'; + const btn = document.querySelector('[data-testid="hatch-chat-compose"]'); + if (!btn) return 'NOT_FOUND'; + btn.click(); + return 'CLICKED'; })()""") if result == 'NOT_FOUND': - print("ERROR: New side chat + button not found (sidebar may be closed)", file=sys.stderr) + print("ERROR: + button [data-testid=hatch-chat-compose] not found", file=sys.stderr) sys.exit(1) # Log which strategy worked for debugging import sys as _sys diff --git a/bin/sidechat_manager.py b/bin/sidechat_manager.py index 6f746ad..fc45c5f 100644 --- a/bin/sidechat_manager.py +++ b/bin/sidechat_manager.py @@ -29,34 +29,31 @@ def _ev(ws, js, await_result=True): def ensure_sidebar(ws, max_retries=3): """ - Ensure the sidebar is open. Returns True if open, False otherwise. - Retries with backoff if the open button isn't found immediately. - Uses data-testid="hatch-chat-switcher-trigger" (reliable selector). - Fixed 2026-10-04: was using text matching which was flaky. + Ensure the chat panel is open. Returns True if open, False otherwise. + Checks for + button presence (not text - avoids false-positive on DM text + containing 'Side chats'). + Fixed 2026-10-04: was checking innerText.includes('Side chats') which matched + DM message text, not the UI. Now checks for [data-testid="hatch-chat-compose"]. """ for attempt in range(max_retries): - # Check if sidebar is already open (Side chats text visible) + # Check if panel is open via + button presence (reliable) is_open = _ev(ws, """(() => { - return document.body.innerText.includes('Side chats'); + return !!document.querySelector('[data-testid="hatch-chat-compose"]'); })()""") if is_open: return True - # Try to open it via data-testid (reliable) - clicked = _ev(ws, """(() => { + # Open via switcher trigger (idempotent) + _ev(ws, """(() => { const btn = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'); - if (btn) { btn.click(); return true; } - // Fallback: text matching - const fb = [...document.querySelectorAll('button')].find(b => - (b.textContent||'').includes('Open chat and side chats')); - if (fb) { fb.click(); return true; } - return false; + if (btn) btn.click(); + return !!btn; })()""") time.sleep(2 * (attempt + 1)) # Exponential backoff: 2s, 4s, 6s # Final check return _ev(ws, """(() => { - return document.body.innerText.includes('Side chats'); + return !!document.querySelector('[data-testid="hatch-chat-compose"]'); })()""") def list_sidechats(ws):