Files
claw/docs/superpowers/specs/2026-04-14-helper-page-lifecycle-hidden-domain-design.md
2026-04-14 08:59:15 +08:00

4.9 KiB

Helper Page Lifecycle Fix & Hidden Domain Support

Date: 2026-04-14 Status: Approved

Problem Statement

Two bugs in the browser-helper.html page management:

  1. Duplicate helper pages: Every WebSocket client reconnection triggers a new serve_client() call, which creates a new LiveBrowserCallbackHost and opens a new helper page via sgBrowerserOpenPage. The old helper page tab is never closed, causing accumulation of orphaned tabs.

  2. Helper page is visible: The bootstrap uses sgBrowerserOpenPage (visible tab API) instead of sgHideBrowerserOpenPage (hidden domain API). The helper page should not be visible to the user.

Root Cause Analysis

Duplicate pages

Call chain:

  • src/service/mod.rs:72 — outer loop accepts new WebSocket connections
  • src/service/mod.rs:79 — each connection calls serve_client()
  • src/service/server.rs:241cached_host declared as local variable, re-initialized to None each call
  • src/service/server.rs:288callback_host.rs:241bootstrap_helper_page() opens a new helper tab

Drop for LiveBrowserCallbackHost (callback_host.rs:321-328) only shuts down the HTTP server thread. It does not send a browser close command for the helper tab.

Visible page

callback_host.rs:28: HELPER_BOOTSTRAP_ACTION = "sgBrowerserOpenPage" — this is the visible-domain open API (API #7). The hidden-domain equivalent is sgHideBrowerserOpenPage (API #6).

Solution: Approach C — Incremental Fix

Step 1: Fix lifecycle (immediate, deterministic fix)

1a. Lift cached_host to outer loop

Move cached_host: Option<Arc<LiveBrowserCallbackHost>> from inside serve_client() to before the loop in run_service() (mod.rs). Change serve_client() signature to accept &mut Option<Arc<LiveBrowserCallbackHost>> instead of creating its own.

Effect: Multiple WebSocket reconnections share the same host. Helper page opens once per process lifetime.

1b. Close helper page on Drop

Enhance Drop for LiveBrowserCallbackHost:

  • Add browser_ws_url: String field to LiveBrowserCallbackHost (stored at construction time)
  • Add use_hidden_domain: bool field (stored at construction time)
  • In Drop::drop, before shutting down the server thread:
    1. Connect to browser_ws_url with 100ms connection timeout
    2. Send register message
    3. Send close command: [helper_url, close_api, helper_url]
      • close_api = "sgBrowserClosePage" when use_hidden_domain == false
      • close_api = "sgHideBrowerserClosePage" when use_hidden_domain == true
    4. All steps are best-effort: failures are silently ignored
    5. Total timeout cap: 500ms

Step 2: Hidden domain config switch (for testing/gradual rollout)

2a. Parameter plumbing

  • LiveBrowserCallbackHost::start_with_browser_ws_url gains parameter use_hidden_domain: bool
  • bootstrap_helper_page selects API based on this flag:
    • true"sgHideBrowerserOpenPage"
    • false"sgBrowerserOpenPage" (current behavior, default)
  • LiveBrowserCallbackHost stores the flag for Drop close-command selection

2b. Caller changes

  • mod.rs / server.rs pass false as default
  • To enable hidden domain, change the call site to pass true

What Does NOT Change

  • callback_backend.rs SHOW_AREA = "show" — JS injection targets visible business pages, not the helper itself
  • sgBrowserExcuteJsCodeByDomain area parameter — stays "show" regardless of helper domain
  • Helper page HTML content — WebSocket connection and command polling JS remain the same
  • collect_lineloss.js — not affected

Affected Files

File Change
src/browser/callback_host.rs New fields on LiveBrowserCallbackHost, start_with_browser_ws_url signature change, Drop enhancement, new close_helper_page helper fn
src/service/mod.rs cached_host lifted to outer loop, passed to serve_client
src/service/server.rs serve_client signature change to accept &mut Option<Arc<LiveBrowserCallbackHost>>
Existing test files Adapt start_with_browser_ws_url calls with new use_hidden_domain parameter

Testing

  • Existing callback_host tests: adapt to new signature (add false parameter)
  • New unit test: use_hidden_domain = true → bootstrap sends sgHideBrowerserOpenPage
  • New unit test: use_hidden_domain = false → bootstrap sends sgBrowerserOpenPage (regression)
  • cargo build + cargo test full verification

Browser API Reference

API Wire format Effect
sgBrowerserOpenPage (API #7) [requesturl, "sgBrowerserOpenPage", url] Opens visible tab
sgHideBrowerserOpenPage (API #6) [requesturl, "sgHideBrowerserOpenPage", url] Opens in hidden domain
sgBrowserClosePage (API #64) [requesturl, "sgBrowserClosePage", url] Closes visible tab
sgHideBrowerserClosePage (API #68) [requesturl, "sgHideBrowerserClosePage", url] Closes hidden domain page