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:
+17
-39
@@ -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
|
||||
|
||||
+12
-15
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user