diff --git a/bin/muse-chat-api.py b/bin/muse-chat-api.py index 30dd097..ecf6e60 100755 --- a/bin/muse-chat-api.py +++ b/bin/muse-chat-api.py @@ -69,7 +69,18 @@ def ev(ws, expr, await_p=False): "id": 1, "method": "Runtime.evaluate", "params": {"expression": expr, "returnByValue": True, "awaitPromise": await_p} })) - resp = json.loads(ws.recv()) + # Drain CDP events until we get our command response (id 1). + # The browser can emit events (Runtime.executionContextCreated, etc.) + # at any time; taking the first recv() blindly returns None on a + # busy page (observed as transient navigation failures in dm.py + # sidechat sends, 2026-10-04 — same class as the NO_SWITCHER fix + # in box-chat-cdp.py commit 8d4bfa7). + for _ in range(50): + resp = json.loads(ws.recv()) + if resp.get("id") == 1: + break + else: + return None return resp.get('result', {}).get('result', {}).get('value') def check_approvals(ws): @@ -215,6 +226,27 @@ def cmd_wait(ws, timeout=30): time.sleep(5) cmd_messages(ws, 2) +def cdp_navigate(ws, url, timeout_s=30): + """Navigate via CDP Page.navigate (proper navigation, waits for commit). + Returns True if the page URL matches the target after navigation.""" + import time as _time + ws.send(json.dumps({"id": 2, "method": "Page.navigate", + "params": {"url": url}})) + # Drain until we get the Page.navigate response (id 2). + for _ in range(50): + resp = json.loads(ws.recv()) + if resp.get("id") == 2: + break + else: + return False + # Wait for the URL to settle (SPA client-side routing). + for _ in range(timeout_s): + cur = ev1(ws, "window.location.href", True) + if cur and url.rstrip("/").lower() in cur.lower(): + return True + _time.sleep(1) + return False + def cmd_sidechat_use(ws, chat_id): """Open a sidechat by name (sidebar text search) or by thread UUID (direct navigation). The sidebar shows titles, not UUIDs, so the @@ -224,15 +256,19 @@ def cmd_sidechat_use(ws, chat_id): if re.fullmatch(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", cid.lower()): url = "https://muse.ai/thread/" + cid.lower() want_uuid = cid.lower() - # Use ev1 (skips CDP chatter) and confirm the URL actually changed. - # A stale read here used to report the previous thread's URL (2026-10-04). + # Use CDP Page.navigate (proper navigation lifecycle) and confirm + # the URL actually changed. Hardened 2026-10-04: the React SPA + # sometimes redirects back to / when the thread page hasn't finished + # loading; retry instead of failing immediately. + # (dm.py sidechat 1/3 flake; was window.location.href via evaluate.) cur = None - for _try in range(3): - ev1(ws, "window.location.href=" + json.dumps(url), True) - time.sleep(4) + for _try in range(5): + ok = cdp_navigate(ws, url, timeout_s=15) cur = ev1(ws, "window.location.href", True) - if cur and want_uuid in cur: + if ok and cur and want_uuid in cur: break + # SPA dropped the nav or hasn't routed yet; wait and retry. + time.sleep(3) print(f"Navigated to: {cur}") return b64 = base64.b64encode(chat_id.encode()).decode() @@ -255,7 +291,15 @@ def cmd_sidechat_use(ws, chat_id): "return window.location.href;" "})()" ) - result = ev(ws, js, True) + # Hardened 2026-10-04: the click sometimes doesn't navigate (React + # mid-render, or the SPA drops it). Retry the whole nav if we didn't + # land on a /thread/ URL. + result = None + for _try in range(3): + result = ev(ws, js, True) + if result and "/thread/" in result: + break + time.sleep(3) print(f"Navigated to: {result}") def cmd_sidechat_main(ws):