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)

This commit is contained in:
operator-main
2026-10-04 03:36:06 +00:00
parent 1cffacc7d1
commit bc4cd77a52
2 changed files with 29 additions and 54 deletions
+17 -39
View File
@@ -263,51 +263,29 @@ def cmd_sidechat_list(ws):
print(chats[:500]) print(chats[:500])
def cmd_sidechat_create(ws): 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. Returns the new thread URL.
Note: Ctrl+J opens the chat panel (which contains Side chats). Selectors (verified 2026-10-04 via DOM investigation):
Fixed 2026-10-04: sidebar is actually the chat panel, toggled by Ctrl+J. - Panel opener: [data-testid="hatch-chat-switcher-trigger"] (idempotent)
- + button: [data-testid="hatch-chat-compose"] (SVG icon, no text)
""" """
import time as _time import time as _time
# Chat panel is always open in the React app - no toggle needed. # Open chat panel via switcher trigger (idempotent - safe if already open)
# Ctrl+J would CLOSE it (it's a toggle). Just proceed to find + button. ev(ws, """(() => {
# Fixed 2026-10-04: removed Ctrl+J, it was closing the always-open panel. const sw = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
# The + button sits next to the "Side chats" header (see screenshot 2026-10-04). if (sw) sw.click();
# It's not labeled "New side chat" - find it by proximity to the header text. return sw ? 'SWITCHER_CLICKED' : 'NO_SWITCHER';
# Wait for React to render the sidebar content first (up to 10s). })()""")
for _wait in range(10): _time.sleep(2) # Wait for React render
_has_header = ev(ws, "document.body.innerText.includes('Side chats')") # Find + button via data-testid (verified selector, SVG icon no text)
if _has_header:
break
_time.sleep(1)
result = ev(ws, """(() => { result = ev(ws, """(() => {
// Strategy 1: Find button with "+" text near "Side chats" const btn = document.querySelector('[data-testid="hatch-chat-compose"]');
const header = [...document.querySelectorAll('*')].find(el => if (!btn) return 'NOT_FOUND';
el.textContent.trim() === 'Side chats' && el.children.length === 0); btn.click();
if (header) { return 'CLICKED';
// 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';
})()""") })()""")
if result == 'NOT_FOUND': 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) sys.exit(1)
# Log which strategy worked for debugging # Log which strategy worked for debugging
import sys as _sys import sys as _sys
+12 -15
View File
@@ -29,34 +29,31 @@ def _ev(ws, js, await_result=True):
def ensure_sidebar(ws, max_retries=3): def ensure_sidebar(ws, max_retries=3):
""" """
Ensure the sidebar is open. Returns True if open, False otherwise. Ensure the chat panel is open. Returns True if open, False otherwise.
Retries with backoff if the open button isn't found immediately. Checks for + button presence (not text - avoids false-positive on DM text
Uses data-testid="hatch-chat-switcher-trigger" (reliable selector). containing 'Side chats').
Fixed 2026-10-04: was using text matching which was flaky. 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): 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, """(() => { is_open = _ev(ws, """(() => {
return document.body.innerText.includes('Side chats'); return !!document.querySelector('[data-testid="hatch-chat-compose"]');
})()""") })()""")
if is_open: if is_open:
return True return True
# Try to open it via data-testid (reliable) # Open via switcher trigger (idempotent)
clicked = _ev(ws, """(() => { _ev(ws, """(() => {
const btn = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]'); const btn = document.querySelector('[data-testid="hatch-chat-switcher-trigger"]');
if (btn) { btn.click(); return true; } if (btn) btn.click();
// Fallback: text matching return !!btn;
const fb = [...document.querySelectorAll('button')].find(b =>
(b.textContent||'').includes('Open chat and side chats'));
if (fb) { fb.click(); return true; }
return false;
})()""") })()""")
time.sleep(2 * (attempt + 1)) # Exponential backoff: 2s, 4s, 6s time.sleep(2 * (attempt + 1)) # Exponential backoff: 2s, 4s, 6s
# Final check # Final check
return _ev(ws, """(() => { return _ev(ws, """(() => {
return document.body.innerText.includes('Side chats'); return !!document.querySelector('[data-testid="hatch-chat-compose"]');
})()""") })()""")
def list_sidechats(ws): def list_sidechats(ws):